Microcontroller Based Interface Design
Part 1
System Engineering Your Robot
Table of Contents
Introduction
In this post I am going to talk about the 3DoT Microcontroller Board and its integration with peripheral devices including sensors, actuators, and serial communications. A basic knowledge of programming in C++ is assumed.
Microcontroller Based Interface Design
Embedded Systems
Engineers design systems. A system can be characterized by a box with an input and output. Typically the engineer is tasked to design the box with a given set of inputs and the desired output.
- When a controller “the brain” is part of the design solution, the design is known as an Embedded System.
- The controller may be implemented using an ASIC (Application Integrated Circuit), FPGA (Field Programmable Gate Array), or in most cases a Microcontroller. A combination of the above on a single IC, is known as a System on a Chip (SoC).
- For this discussion, the input device is by definition a Sensor, and the output device an Actuator.
In this post we look at the microcontroller based system design used by our robots. It is hoped that by looking at this specific example you will be able to apply the lessons learned to the design of other microcontroller-based systems.
Robot Microcontroller Based System Design
Figure 2.0 illustrates our robot design from a generic capabilities perspective. At the heart of our embedded system is an ATmega32U4 Microcontroller.
The ATmega32U4 is built into the Arduino compatible 3DoT (3D of Things) Board. Let’s take a closer look at the hardware and software built into the 3DoT board.
Hardware
The 3DoT board is a micro-footprint 3.5 x 7 cm all-in-one Arduino compatible microcontroller board, that integrates all the components needed to create a small autonomous, RC controlled, or telepresent robot.
- Microcontroller: At the heart of the 3DoT board is an Arduino Leonardo based microcontroller unit (ATmega32U4), featuring an 8 MIPS (Millions of Instructions Per Second) 2-stage pipelined AVR RISC processor.
- USB (Universal Serial Bus) serial communications circuitry allows you to upload your programs and download data.
- An FCC-certified BLE 5.0 module (optional) allows wireless communication with the ArxRobot Android/iPhone App and Arxterra control panel.
- Power Management: Power for the 3DoT board is provided by an RCR123A LiPo 650 mAh rechargeable battery with associated power protection, switching, and conditioning circuitry (3.3v and 5v). Plus, an integrated 3.7v Li-ion battery charger with battery level sensor circuit. You also have access to an external battery connector – for input voltages between 4 – 18 V.
- Motors & Servos: To control your robot’s motors the 3DoT board includes a DRV8848 Dual Motor Driver. JST Connectors are provided for up to two (2) DC Motors and two (2) Micro or Ultra-Micro Servos.
- Expansion: Customize your robot with 8-pin forward sensor and 16-pin 3DoT Shields, also known as daughter boards.
- 16-pin top female headers for shields – providing I/O, I²C, SPI, USART, 3.3 V and 5 V.
- The forward-facing 8-pin female header for sensor shields – providing 4 analog pins, I²C, and 3.3 V power – for sensor shields like infrared or metal-detecting shields. Great location for headlights, lasers, ultrasonics, etc.
- Programming switch: Three-position switch for easy programming
- No more double-tapping a button and rushing to program your board, or your robot trying to drive away while programming. Set the switch to PRG to program, RUN to execute your code.
Software
- Programs are written in the Arduino IDE (Integrated Development Environment).
- Pre-installed Arxterra 3DoT bootloader plus Robot3DoTBoard library – fully customizable for your robot
- Together this software suite running on the ATmega 32U4 microcontroller allows you to control a 2-wheel robot out-of-the-box using the FREE ArxRobot Android/iPhone App.
- If the robot can hold your phone, then you can remotely control your robot from anywhere with an internet connection from the Arxterra Control Panel.
Document Objective
The objective of this document is to teach you how to interface peripheral devices to a microcontroller based system. We first…
- Allocating the resource of our MCU (Micro-Controller Unit).
- Look at how sensors and actuators could be incorporated into this design
- Present the reference design as an example.
Allocating MCU Resources
Our embedded system is comprised of a 3DoT Microcontroller Board. At the heart of the system is the ATmega32U4 microcontroller. The 3DoT board consumes some of the resources of the ATmega32U4 in exchange for extending the capabilities of the integrated system. Table 1 provides a mapping of these resources and ultimately the interface resources available to the sensors and actuators of your robot in columns G through K. Columns L and M show how the 16-pin 3DoT and 8-pin sensor headers can be used to add an IR sensor and wheel encoder shields.
ATmega32U4 I/O pins (columns A and B)
The ATmega32U4 comes in a 44-pin QFN (Quad Flat No-leads) and a TQFP (Thin Quad Flat Package). The arrangement of these pins around the QFN or TQFP IC is shown in Figure 3.0. Each pin is assigned both a number and a name.
Due to pin-out limitations of the IC package (QFN and TQFP), most I/O pins of the ATmega32U4 are multiplexed. Specifically, they can be programmed to provide different interfaces to the system. For example, pin 41 PF0 (ADC0) can be wired to a digital I/O device like a button or and LED. Conversely it could be wired to an analog input to measure a voltage output by an IR sensor.
Figure 3.0 ATmega32U4 Pin-out
Table 3.0 allows us to place the ATmega32U4 within the context of our design. Specifically, how are we going to wire up all the actuators and sensors to our microcontroller IC. We begin by adding each pin number and its name to columns 1 and 2 respectively.
As shown in Table 3.0 most but not all rows may be assigned as I/O pins. For example the VCC, GND, USB, XTAL (crystal) and reset pins are not available. These non-I/O pins appear as Gray rows in the table. What this means to the system designer instead of having 44 Multiplexed I/O pins you now only has twenty twenty-six (26). All 26 of these multiplexed pins may be used as digital I/O pins (PB7-0,PC7-6,PD7-0,PE6 and PE2, PF7-4 and PF1-0). These I/O pins appear as Orange rows in the table. When one of these I/O pins is consumed, the remainder of the row is White.
Arduino I/O Pins (column C)
The Arduino was designed as a tool for introducing students to embedded systems. To a student unfamiliar with peripheral subsystems like GPIO (General Purpose Input Output) ports and an ADC (Analog to Digital Converter), the names assigned to the pins of the ATmega32U4 can be intimidating. Instead, the creators of the Arduino realized that these complex electronic systems could be introduced in a step-by-step fashion by starting with the function to be performed. Put another way, students new to engineering are more interested in “what” the pin can do, rather than “how” it does it. For this reason, the Arduino creators renamed the pins by function. For example, pins that work with digital binary inputs and outputs start with the letter D and are numbered sequentially (D0 to D23). Pins that work with Analog inputs start with the letter A and are again numbered sequentially (A0 – A10). Along with these “functional” names came simple program instructions, like digitalRead(D0) and digitalWrite(D1) for digital signals and analogRead(A0) for analog inputs.
The 3DoT instruction set builds on this functional vs. technical philosophy by allowing you to control your robot’s movements with a single move() instruction.
Table 1.0 Column C shows the Arduino names assigned to each ATmega32U4 I/O pin (orange rows). Specifically, digital pins D0 to D23 and analog inputs A0 to A11. The single exception is pin 33 PE2 (HWB) which is set to 0 (wired to ground) for Arduino ATmega32U4 microcontroller boards. The 3DoT board wires this pin to its ON/PROG/RUN switch. This change was made to make the 3DoT board easier to program than its Arduino cousins.
3DoT
To build an Arduino-based telerobotic Robot, in addition to the microcontroller board you would need a battery, charger, Bluetooth shield, motor shield, and a breadboard containing miscellaneous parts. The 3DoT board integrates all these components on a single 3.5 cm x 7 cm board. As a part of your robot, all these built-in functions consume microcontroller pin resources as defined in columns D, E, and F. In most instances, once used these resources are no longer available to implement other functions.
DRV8848 Motor Driver (column F)
Figure 3.1 shows how the DRV8848 Dual Motor Driver is wired to a typical MCU. Looking at Table 3.0 column F we see that our motor driver is wired to pins pin 25 to 31 of the ATmega32U4 MCU. Also notice that after this column these rows are colored white indicating that the resources are no longer available to the system.
EH-MC17 Bluetooth Module (column E)
The EH-MC17 (Figure 7) sends and receives data to the USART peripheral subsystem of the MCU. The USART serial interface only needs 2 wires (RX and TX) to physically implement this interface. As shown in column B of Table 3.0 the TX1 and RX1 pins are internally routed from the USART peripheral subsystem to pins 20 and 21 of the ATmega32U4 IC. A few additional things are of note here.
- To add this Bluetooth module we need to sacrifice Arduino digital pins D0 and D1.
- Unlike the motor driver, these two rows do not immediately show these pin resources as being consumed. Specifically, they are brought out to the 3DoT Shield Header J1 (column G). The reason is simple. The addition of the HM-11 module to the 3DoT board is optional. If the robot designer chooses to not solder the HM-11 module to the bottom of the 3DoT board, then they are free to use these pins to implement a different serial communication standard (e.g. XBee, WiFi) or for a fully autonomous robot, Arduino digital pins Do and D1 are again available.
- It is curious to note that the MCU d0/RX1 pin is wired to the HM-11 TX pin and that the MCU d1/TX1 pin is wired to the HM-11 RX pin. Confusing isn’t it and also a very common wiring error. You can think of it this way; one person’s output is another person’s input.
LEDs, Switch, and Battery Level Circuit (column D)
Beyond the Motor Driver (Table 3.0 column F) and the HM-11 Bluetooth module (Table 3.0 column E) the 3DoT Board uses a few additional MCU pin resources for LEDs (Light Emitting Diodes), the ON/PROG/OFF Switch, and the Battery Level circuit. Let’s take a closer look at column D of Figure 3.0.
LEDs
RX LED 4 and TX LED 6 are wired to pin 8 (Arduino pin D17/RXLED) and pin 22 (Arduino pin TXLED). The famous Arduino on-board LED typically uses by the BLINK script, is wired to pin Port D bit 5, not Arduino digital pin 13.
DP3T OFF/PROG/RUN Switch
The Arduino Leonardo and in fact all Arduino 32U4 based boards have a peculiar problem as a result of the ATmega32U4 incorporating the USB peripheral system as part of the MCU. On “traditional” Arduino boards, like the UNO, a separate ATmega MCU is dedicated to implementing the USB interface. As a result, ATmega32U4 based Arduinos must juggle between the user uploading programs and acting as a USB port for the user’s program. You need the first function when you are programming and the second if you want your program to interface to the computer. For example, if you want your program to output to the Arduino IDE’s Serial Monitor. The Arduino developers solved this problem by having the Arduino upon reset look for the user to upload a program and after a few seconds, if the user does not start the upload process then it switches to running the currently loaded program. In many cases, the user can not initiate this upload process fast enough for the Arduino’s liking resulting in the “double-tap” solution which only works part of the time. To solve this problem 3DoT developers implemented a different solution. The 3DoT board has a DP3T (Double Pole Triple Throw) Switch. As the name implies this switch has three positions (OFF/PROG/RUN). Now the ATmega32U4 bootloader, by interrogating pin Pin 33 PE2 (HWB), can directly determine if you want to upload a program (PROG) to the ATmega32U4 or RUN your program.
Battery Level Circuit
Going back to Table 3.0 column D row/pin 41 we see VBATT_LVL. Which is short for “Voltage Battery Level.” As the name implies the function of this pin is to read the current battery voltage. A simple voltage divider is all it takes to implement the battery level circuit, with the top of the voltage divider wired to the positive lead of the battery and the center of the divider wired to pin 41. The output of this circuit is an analog voltage typically between 2.1 and 1.2 volts. Going back to pin 41 we see that the ATmega32U4 names this pin PF0 (ADC0), while the Arduino names the pin A5/D23. The key here is that the pin can be both an analog input or digital input or output. So to implement this function, we will be using the Analog to Digital (ADC) peripheral subsystem of the ATmega32U4 and not the General Purpose I/O (GPIO) port used for digital inputs and outputs.
Robot
In this section we are going to look at the MCU resources available for customizing your robot. As already mentioned the 3DoT board directly supports a robot with two (2) DC motors and two (2) micro-servos. The DC motors are controlled by the TB6612FNG motor driver IC whose pins have already been allocated. The micro-servos have their own dedicated connector J7 (Figure 4.1), and wire directly to the ATmega32U4 through limiting resistors. The limiting resistors protect the MCU from damage in the event of an accidental short for the V7 boards.
Customization of your robot is afforded by three 8-pin connectors J1, J2, and J3. Daughter boards that plug into connectors J1 and J2 will be called “3DoT Shields.” Daughter boards that plug into connector J3 will be called “3DoT Sensor Shields.”
3DoT Shields
Connectors J1 and J2 are located on the top of the board in a fashion similar to the Arduino UNO, and like the UNO can support one or more stackable 3DoT Shields. This is especially true of the 3DoT board, whose J1 and J2 connectors support three (3) different serial communication protocols: I2C, SPI, and USART. To take this to the extreme, you could theoretically stack 127 I2C shields without even mentioning SPI and USART shields. In addition, the J2 pins 5 and 6 support one (1) Analog signal with an associated reference voltage (AREF). To avoid conflicts with 3DoT Sensor Shield (connectors J3) on the bottom of the board, do not wire digital I/O to I2C pins SDA and SDL (D2, D3). In addition, if your robot comes with the HM-11 Bluetooth module do not wire digital I/O to USART pins RX and TX (D0, D1).
As we have seen, the multiplexing of the I/O pins immediately forces the system engineer to make trade-offs in the design of their shields. For example, If your making a fully autonomous robot (no HM-11 Bluetooth module), you do not need the single analog input A4 on connector J2, and are your forward 3DoT sensor shield is not using I2C pins SDA and SCL, then your 3DoT Shield could have a maximum of nine (9) digital I/O signals. In most designs all those conditions will not be met, and you will have to trade-off one MCU peripheral subsystem (see Figure 4.2) against another (GPIO Port, A/D Converter, I2C, SPI, and USART).
3DoT Sensor Shields
Connector J3 is an 8-pin connector located on the front bottom of the board. This connector is designed for “sensor shields” located on the front of the robot. Typical applications would include line following robots. With sensors in mind, the connector includes four analog inputs (A0 to A3) and support for the I2C serial communication protocol (SCL, SDA). For sensor shields requiring digital I/O lines it is recommended that one or more of the analog pins (D18 to D21) be utilized. As mentioned in the previous paragraph, to avoid conflicts with 3DoT Shields (connectors J1 and J2) on the top of the board, do not wire digital I/O to the I2C pins (D2, D3).
Figure 4.2 ATmega328P Block Diagram
In the future when I talk about Arduino peripheral subsystems, as I did in the previous paragraph, I will show the ATmega328P Block Diagram in place of the ATmega32U4 processor used on the 3DoT board. The ATmega328P microcontroller was used on the original Arduino UNO and is far simpler than the ATmega32U4. Therefore, I can make the same points using the ATmega328P, without adding unnecessary complexity to the topic under discussion. |
Next Step
Applying what you learned here, in “Microcontroller Based Interface” Design Part 2 we will look at how sensors and actuators can be incorporated into your robot.
Lecture 2 – Robot Sensors and Actuators
System Engineering Your Robot
Table of Contents
Interfacing Sensors and Actuators
Reading
- Microcontroller Interfacing Circuits by Revolution Education Ltd.
- For help with a specific interface, or just to look for ideas, visit the related Arduino Forum.
- A detailed discussion of Pulse Width Modulation is beyond the scope of the document. We will be covering PWM in more detail later in the semester. For now you may want to read this nicely illustrated article: Working with Atmel AVR Microcontroller Basic Pulse Width Modulation (PWM) Peripheral
Introduction
In Part 1 we developed a Resource Map for our rover (Table 1.0). In this section we look at how sensors and actuators could be incorporated into the design of our rover. Specifically, we will map our sensors and actuators to our I/O pins.
Table 1.0 Rover Resource Summary
While Part 1 of this System Interface Design discussion was from the perspective of the ATmega328P; Part 2 will be presented from the perspective of the I/O device.
A detailed discussion of the ATmega328P subsystems, along with the software required to run them, is outside the scope of this document and will be covered in future lectures (hopefully).
Sensors
Digital Interface
Design Example: DIP Switch
A DIP switch is an example of a digital sensor which you could add to your design. For example to allow your robot to discover its mode of operation upon reset.
If you are not planning to implement the SPI interface and you do not have any Analog inputs, then working from the schematic of the device and the interface matrix, I would recommend wiring the four (4) Single Pole Double Throw (SPDT) DIP switch, shown in 1.0, to GPIO Pins (PF1, PB1, PB3, PB2, and PB1) of the ATmega32U4 or in Arduino parlance digital pins D22, D14, D16, D15. Button resources (Figure 2.1) would be allocated in a similar fashion.
Figure 1.0 Two SPDT DIP Switches
Figure 1.1 SPST Button Schematic
Parallel Interface
For our robots we are limited to no more than 9 digital inputs (see Section “3DoT Shields” in Part 1 of this series). Consequently, while wiring 1 button to the ATmega32U4 is not a problem, wiring a parallel device with a large word size is not directly possible. For example an 8-bit A/D converter would consume 89% of our available I/O resources. A much better solution would be an A/D converter that supports the I2C interface. Here we could get a 12-bit A/D converter with no loss in pin count.
Analog Interface
Many sensors output an analog voltage, including our IR sensors. The ATmega32U4 has a single ADC subsystem whose input can come from up to 6 multiplexed channels (ADC0 to ADC5). The reference design preserves all 6 of these analog channels. From a practical standpoint, the term multiplexed means that although our design can support up to 6 analog sensors, we can only read one at a time.
Design Example
As with all devices, start with a schematic
Figure 2.3 Medium Range IR Sensor Block Diagram
From the block diagram it is seen that the interface of an IR sensor is a single analog wire. In the absence of any other resource requirements, we are free to wire this sensor output to any one of our six analog inputs (ADC0 to ADC5).
Voltage Range and Electromagnetic Interference (EMI)
When working with any analog interface you should be sensitive to the output swing of the analog signal and to the introduction of noise.
For our medium range IR sensor, the Voltage Output peaks at around 3.1 V. That means to maximize our resolution we will want to use an external voltage reference of 3.3V. The full scale reading of the ADC can be set to the AVCC (5 v), AREF, or an internal 1.1v reference voltage. So in this case we will want to wire a 3.3v reference source to AREF. In a future lecture you will also find we need to place a limiting resistor between the our reference source and AREF.
Noise is almost always a concern when working with an analog signal. For our IR sensor the data sheet recommends a 10 uF capacitor be placed as closely as possible to the sensor. In addition, analog signals are often sent over a twisted and shielded cable.
Actuators
Digital Interface
All ten available General Purpose I/O (GPIO) pins may be configured as outputs. The output circuit of the ATmega328 can sink or source a respectable 20 mA. This means that the ATmega328P can directly turn on/off LED indicators, without the need for an external driver (you will still need a limiting resistor).
Figure 3.0 Diode Circuit
Another, digital actuator is our 650nm 1mW 8x13mm Laser Module. Once again before you purchase a device, make sure you have a datasheet. In the case of an laser, you need to know if it includes a current source, in which case you only need to turn the laser on/off, Otherwise, you will need to design in your own current source..
Figure 3.1 Laser Diode constant Current Circuit (source: Sparkfun Forum)
Motor and Pulse Width Modulation (PWM ) Interface
Motor On/Off
Change Motor Direction
Change the Speed of a Motor
A simple variable resistor is all you need if you want to control the speed of a DC motor manually. To control the speed of a DC motor with a microcontroller you will use one of our six PWM channels (e.g., PWM1A, PWM1B).
Figure 3.4 A simple method to generate the PWM pulse train corresponding to a given signal is the intersective PWM: the signal (here the red sinewave) is compared with a sawtooth waveform (blue). When the latter is less than the former, the PWM signal (magenta) is in high state (1). Otherwise it is in the low state (0).: Wikipedia
All the Above
If you want to turn your DC motor on/off, change direction, and control the speed of the motor you will need two (2) GPIO pins to configure the H-Bridge and a single PWM channel to control the speed. Without the Adafruit motor shield, If you wanted to control 4 DC motors (or 2 DC motors and a Bi-polar stepper motor) you would need eight (8) GPIO pins plus 4 PWM channels. With the motor shield we only lost 4 GPIO pins – we still need 4 PWM channels.
Attaching a DC motor to the Adafruit motor shield is as simple as wiring your DC motor to one of the four (4) Motor control connector pairs.
Servos
Reprinted from : Microcontroller Interfacing Circuits by Revolution Education Ltd.
A typical servo has just three connection wires, normally red, black and white (or yellow). The red wire is the 5V supply, the black wire is the 0V supply, and the white (or yellow) wire is for the positioning signal. The positioning signal is a pulse between 0.75 and 2.25 milliseconds (ms) long, repeated about every 18 ms (so there are roughly 50 pulses per second). With a 0.75ms pulse the servo moves to one end of its range, and with a 2.25ms pulse the servo moves to the other. Therefore, with a 1.5 ms pulse, the servo will move to the central position. If the pulses are stopped the servo will move freely to any position. Unfortunately servos require a large current (up to 1A) and also introduce a large amount of noise on the power rail. Therefore as with all motors, the servo should be powered from a separate power supply. Remember that when using two power supplies the two ground rails must be joined to provide a common reference point.
Serial Interface
The ATmega328P supports three serial interface protocols: Universal Asynchronous Receiver/Transmitter (USART), Serial Peripheral Interface (SPI), and Inter-Integrated Circuit (I2C). All three support two-way communications. This means that all three serial subsystems of the ATmega328P can work as easily with actuators as sensors (see Section 2.1) which implement one or more of these interface types.
Universal Asynchronous Receiver/Transmitter (USART)
For the purposes of this study, the USART will be reserved for communications between the rover and the PC. As we learned in Part 1, the USART subsystem uses two IC pins (TXD and RXD).
Serial Peripheral Interface (SPI)
The SPI Subsystem of the ATmega328P requires four pins to implement 2-way serial communications (SCK, MISO, MOSI, SS). These lines are are wired to pins PB5 (SCK/PCINT5), PB4 (MISO/PCINT4), PB3 (MOSI/OC2A/PCINT3), and PB2 (SS/OC1B/PCINT2). Of interest here are Output Compare signals OC2A and OC1B. These signals are from the 8-bit Timer 2 and 16-bit Timer 1 subsystems. Both timer subsystems are used by the Adafruit motor shield to generate Pulse Width Modulated signals PWM2A and PWM1B. Pulse Width Modulation is critical for controlling the speed of DC motors (PWM2A) and setting the angle of a servo (PWM1B). Consequently, Adafruit implemented their SPI interface with General Purpose I/O (GPIO) ports and software. In this way they were able to maximize the number of PWM signals available to the shield, while sacrificing the SPI subsystem of the ATmega328P.
So what if you are working with a peripheral device that implements the SPI serial communications protocol? First, you can follow the Adafruit path and implement your SPI interface in software. Second, the SPI subsystem can be recovered at the cost of some functionality or the time sharing of shared resources. An example of the last case would be to communicate with your SPI device only when the shared motor control signal was not required (motor is off).
Inter-Integrated Circuit (I2C)
The I2C or TWI in Atmel speak, is a serial communications protocol with similar functionality to the SPI communications protocol. However, unlike the SPI which requires 4 pins to implement two-way communications, the I2C needs only two pins (SDA, SCL). The trade-off here is in the added complexity of the I2C interface. Today many intelligent sensors and actuators support both the I2C and SPI interface. This is the case with our L3G4200D 3-Axis Gyro Carrier with Voltage Regulator. As illustrated in Table 1.0 , the pins of the ATmega328P I2C subsystem are available for our sensors and actuators.
It should be noted that the I2C interface supports up to 128 devices without the need for any additional I/O pins.
Recovering I/O Resources
TBS
Rover Example
For our Rover we have the following Sensors and Actuators.
Sensors
I2C serial interface communicating with 3-Axis Gyroscope
I2C serial interface communicating with Current Sensor (optional)
I2C Arduino Nano (optional)
Analog input from Mid Range IR
Analog input from Long Range IR
Analog input from RC Circuit to 7.2V NiCD Battery – Dirty Power
Analog input from RC Circuit to 9V NiMH Battery – Clean Digital
Analog input from RC Circuit to 9V NiMH Battery – Camera
Four (4) Digital inputs from two (2) Shaft Encoders. Assumes full resolution of quadrature shaft encoders.
Actuators
Two (2) PWM outputs to H-Bridges controlling bipolar Stepper Motor – Adafruit Motorshield
Two (2) PWM outputs to H-Bridge controlling two DC Motors – Adafruit Motorshield
One PWM output to Servo – Adafruit Motorshield
One Digital output thru a Transistor circuit to turn the Laser on/off
One Digital output thru a Transistor circuit to turn the Camera on/off
(One or more Digital outputs to address bits of a 4051 Analog MUX for camera battery)
For our example we begin by allocating resources interfaced to ATmega328P peripheral subsystems requiring the use of only one set of pins. In this case the I2C serial interface. Looking at the matrix we see that this first step removes two Analog inputs leaving us with four (4). We need five (5) Analog Inputs so we are already in trouble. We will leave the sensors and actuators wired to the GPIO peripheral subsystem to the end. This is because these are the simplest to assign.The PWM channels are output from the Adafruit Motor Shield and so easily assigned. Once again we have very few options with respect to which pins can be used. We are now left with 4 Digital Input and 2 Digital Output pins to be connected to the GPIO peripheral subsystem of the ATmega328P. So we need to find 6 (4 inputs + 2 outputs) GPIO pins. Looking at the matrix we see that we have 3 GPIO pins left. So we are three (3) I/O pins short. How we find the 1 analog and 3 GPIO pins is left up to you. Hint: Look-up 4051, 74HCT595, and parallel shift registers like the 74HC166, 74HC194, and CD4014.
Rover with Arduino Nano Example
In this design example to the chassis mounted Arduino Uno we add a Arduino Nano to the scan/tilt platform.
Scan / Tilt Platform
Sensors
Analog input from Mid Range IR
Analog input from Long Range IR
Analog input from RC Circuit to 9V NiMH Battery – Camera
Actuators
One PWM output to Servo – Adafruit Motorshield
One Digital output thru a Transistor circuit to turn the Laser on/off
One Digital output thru a Transistor circuit to turn the Camera on/off
Serial Communication
For our example we begin by allocating resources interfaced to ATmega328P peripheral subsystems requiring the use of only one set of pins. In this case the I2C serial interface. Looking at the matrix we see that this first step removes two Analog inputs leaving us with four (4). We need two (2) Analog Inputs so we have two (2) pins which may be used for sensors and actuators to be wired to the GPIO peripheral subsystem (PC 2 – 5). Four (4) of our six (6) PWM channels are required by the Adafruit Motor Shield. This leaves us with two (2) more pins which may be used for sensors and actuators to be wired to the GPIO peripheral subsystem. Looking at the matrix we see that we have 2 GPIO pins PD2 and PB5 which are unused, giving us a total of six (6) pins available to the GPIO peripheral subsystem. We only need four (4) for our two shaft encoders which means we have two (2) spare pins.
Table 3.0 System Resource Map for Chassis Mounted Arduino Uno
Software Programming in C++: Introduction
PDF Lecture http://web.csulb.edu/~hill/ee444/Lectures/02%20C++%20Introduction.pdf
READING
The AVR Microcontroller and Embedded Systems using Assembly and C
by Muhammad Ali Mazidi, Sarmad Naimi, and Sepehr Naimi
Sections: 7.1, 7.3, 7.4, 7.6
Here is a fun tool to translate your C++ code into Assembly
https://godbolt.org/
Table of Contents
Assemblers, Compilers, and Interpreters
Language Levels
- Scripting Arduino, Matlab
- High-Level JAVA, PYTHON
- Mid-Level C/C++
- Low-Level Assembly
From Humans to the Machine
Reference: https://en.wikipedia.org/wiki/Interpreted_language
Assembly 1:1 Machine Code
Compilers 1:X Machine Code
Language examples = C/C++, Python, BASIC
Interpreters 1:1 bytecode
Language examples = C#, Java, Python, BASIC
Examples of Languages which may be Compiled or use an Interpreter
Most interpreted languages use an intermediate representation, which combines compiling and interpreting.
- JavaScript
- Python
- Ruby
VARIABLE PROPERTIES
- Data Type
- Scope
Scope
Scope allows the compiler to help us from making mistakes (overwriting the value of a variable) & allows us to help the compiler optimize the code (manage SRAM resources).
Reference: https://andrewharvey4.wordpress.com/tag/avr/
Supplemental figure for Volatile
The key here may be the destruction of the mapping of variables to registers in the interrupted program.
Supplemental figure for setting and clearing bits
Reference: http://web.csulb.edu/~hill/ee346/Lectures/14%20AVR%20Logic%20and%20Shift.pdf
Data Types
Explicit Data Types
source: Wikipedia stdint.h
- The C standard library introduced in the C99 standard library (stdint.h) allows programmers to write more portable code by allowing them to specify exact-width integer types, together with the defined minimum and maximum allowable values for each type.
- This new library is particularly useful for embedded programming which often involves considerable manipulation of hardware specific I/O registers requiring integer data of fixed widths, specific locations and exact alignments.
- The naming convention for exact-width integer types is intN_t for signed integers and uintN_t for unsigned integers. For example
uint16_t revsteps; // # steps per revolution
uint8_t steppernum;
uint32_t usperstep, steppingcounter;
Implicit and Architecture Dependent Data Types
Data Type | Size in Bits | Data Range / Usage |
---|---|---|
void | ||
boolean | ||
char | 8 | -128 to +127 |
unsigned char | 8 | 0 to 255 |
byte | 8 | 0 to 255 |
int | 16 | -32,768 to +32,767 |
unsigned int | 16 | 0 to 65,535 |
word | 16 | 0 to 65,535 (Arduino) |
long | 32 | -2,147,483,648 to +2,147,483,648 |
unsigned long | ||
float | 32 | +/-1.175e-38 to +/-3.402e38 |
double | 32 | +/-1.175e-38 to +/-3.402e38 |
string – char array | ||
String – object | ||
array |
Utilities
sizeof()
The sizeof operator returns the number of bytes in a variable type, or the number of bytes occupied by an array.
VARIABLE SCOPE
Variable Scope
- Variables in the C programming language, which Arduino uses, have a property called scope.
- A Global variable is one that you can access anywhere in a program. Local variables are only visible to the function in which they are declared. In the Arduino environment, any variable declared outside of a function.
- Local variables insure that only one function has access to its own variables. This prevents programs from inadvertently modifying variables used by another function.
- A variable declared inside brackets {} can only be accessed within said brackets.
Source: Arduino – Variable Scope
uint gPWMval; // any function will see this variable
void setup()
{
// …
}
void loop()
{
int i; // “i” is only “visible” inside of “loop”
float f; // “f” is only “visible” inside of “loop”
// …
for (int j = 0; j <100; j++){
// variable j can only be accessed inside the
// for-loop brackets
}
}
QUALIFIERS
Static
- The static keyword is used to create variables that are visible to only one function. However unlike local variables that get created and destroyed every time a function is called, static variables persist beyond the function call, preserving their data between function calls.
- Variables declared as static will only be created and initialized the first time a function is called.
source: COMP2121: Microprocessors and Interfacing
http://www.cse.unsw.edu.au/~cs2121
int randomWalk(int moveSize){
static int place; // variable to store value in random walk,
// declared static so that it stores
// values in between function calls, but
// no other functions can change its value
place = place + (random(-moveSize, moveSize + 1));
// check lower and upper limits
if (place < randomWalkLowRange){ place = randomWalkLowRange; } else if(place > randomWalkHighRange){
place = randomWalkHighRange;
}
return place;
}
Volatile
- The volatile qualifier directs the compiler to load the variable from RAM and not from a general purpose register (R0 – R31),
- A variable should be declared volatile when used within an Interrupt Service Routine (ISR).
- For more on Interrupts visit Gammon Software Solutions forum
// toggles LED when interrupt pin changes state
int pin = 13;
volatile int state = LOW;
void setup()
{
pinMode(pin, OUTPUT);
attachInterrupt(0, blink, CHANGE);
}
void loop()
{
digitalWrite(pin, state);
}
void blink()
{
state = !state;
}
Const
Reference: The C++ ‘const’ Declaration: Why & How
- The const qualifier declares a variable as a constant.
Example
// create integer constant myConst with value 33
const int myConst=33;
- Such constants are useful for parameters which are used in the program but do not need to be changed after the program is compiled.
- It has an advantage over the C preprocessor ‘#define’ command in that is understood and used by the compiler itself.
- Use of constant checked for scope.
- Use of constant checked for datatype.
- As a result error messages are much more helpful.
- Example usage is in the definition of a base pointer to an array
Example
// The compiler will replace any mention of
// ledPin with the value 3 at compile time.
#define ledPin 3
- Constants are saved in SRAM not in Flash Program Memory (C was originally designed for Princeton based Machines). Click here to learn how to save data to Flash Program Memory.
Arduino Scripting Language
Reference
Arduino Language Reference
Arduino programs can be divided in three main parts: structure, values (variables and constants), and functions.
Structure
Control Structures
Further Syntax
- ; (semicolon)
- {} (curly braces)
- // (single line comment)
- /* */ (multi-line comment)
- #define
- #include
Arithmetic Operators
Comparison Operators
- == (equal to)
- != (not equal to)
- < (less than)
- > (greater than)
- <= (less than or equal to)
- >= (greater than or equal to)
Boolean Operators
Pointer Access Operators
Bitwise Operators
- & (bitwise and)
- | (bitwise or)
- ^ (bitwise xor)
- ~ (bitwise not)
- << (bitshift left)
- >> (bitshift right)
Compound Operators
- ++ (increment)
- — (decrement)
- += (compound addition)
- -= (compound subtraction)
- *= (compound multiplication)
- /= (compound division)
- &= (compound bitwise and)
- |= (compound bitwise or)
When we start constructing compound assignment statements in order to assign values to fields within a peripheral subsystem register, also known as a special function register (SFR), it is important to remember operator precedence.
Level | Operator | Description | Grouping |
1 | :: | scope | Left-to-right |
2 | () [] . -> ++ — dynamic_cast static_cast reinterpret_cast const_cast typeid | postfix | Left-to-right |
3 | ++ — ~ ! sizeof new delete | unary (prefix) | Right-to-left |
* & | indirection and reference (pointers) | ||
+ – | unary sign operator | ||
4 | (type) | type casting | Right-to-left |
5 | .* ->* | pointer-to-member | Left-to-right |
6 | * / % | multiplicative | Left-to-right |
7 | + – | additive | Left-to-right |
8 | << >> | shift | Left-to-right |
9 | < > <= >= | relational | Left-to-right |
10 | == != | equality | Left-to-right |
11 | & | bitwise AND | Left-to-right |
12 | ^ | bitwise XOR | Left-to-right |
13 | | | bitwise OR | Left-to-right |
14 | && | logical AND | Left-to-right |
15 | || | logical OR | Left-to-right |
16 | ?: | conditional | Right-to-left |
17 | = *= /= %= += -= >>= <<= &= ^= |= | assignment | Right-to-left |
18 | , | comma | Left-to-right |
Values
Constants
Data Types (see above)
Functions – Scripting Language
Digital I/O
- pinMode() // Writes to GPIO DDR register
- digitalWrite() // Writes to GPIO Port register
- digitalRead() // Reads GPIO Pin register
Analog I/O
- analogReference() // Writes to bits REFS1:REFS0 of ADMUX register
- analogRead() // Reads ADC Data register (ADCH/ADCL)
- analogWrite() // Writes to OCRnA or OCRnB registers of Timers
Advanced I/O
- tone()
- noTone()
- shiftOut() // Software implementation of SPI MOSI interface
- shiftIn() // Software implementation of SPI MISO interface
- pulseIn()
Time
- millis()
- micros()
- delay()
- delayMicroseconds()
Math
Trigonometry
Random Numbers
- randomSeed()
- random()
Bits and Bytes
External Interrupts
- attachInterrupt() // Configures external interrupt pins INT1 and INT0
- detachInterrupt() // Clears EIMSK register bits INT1 and INT0
Interrupts
- interrupts() // Sets SREG I bit
- noInterrupts() // Clears SREG I bit
Communication
WORKING WITH BITS IN C++
Resources
1. Arduino
2. AVR-libc
Set/Clear a Bit
Assembly
GPIO Port (first 32 I/O addresses)
sbi PORTB, PB3 | cbi PORTB, PB3 |
IO Address Space
in r16, PORTB sbr r16, 0b00001000 out PORTB, r16 |
in r16, PORTB cbr r16, 0b00001000 out PORTB, r16 |
// Set a Bit | // Clear a Bit |
digitalWrite(MOTORLATCH, HIGH); | digitalWrite(MOTORLATCH, LOW); |
// Set a Bit | // Clear a Bit |
PORTB |= _BV(PB3); or sbi(PORTB, PB3); // deprecated |
PORTB &= ~_BV(PB3); or cbi(PORTB, PB3); // deprecated |
The AVR C library includes the following definition
#include
// _BV Converts a bit number into a Byte Value (BV).
#define _BV(bit) (1 << (bit)) // : Special function registers
C/C++
// Set a Bit | // Clear a Bit |
PORTB |= (1 << (PB3)); | PORTB &= ~(1 << (PB3)); |
Set a Bit Pattern
ADC Subsystem ADMUX Register Example
In this example we set/clear 2 fields within a byte. Undefined bits are cleared.
ADMUX = (analog_reference << 6) | (pin & 0x0f); // analogRead
Test Your Knowledge 1: Assume a function of type uint16_t. What would be returned if high = A and low = 3?
return (high << 8) | low;
0x0A03
Test Your Knowledge 2: How could you modify the ADMUX example to allow the programmer to set or clear the ADC Left Adjust Result ADLAR bit? Answer
ADMUX = (analog_reference << 6) | (pin & 0x0f) | ((left_adjust & 0x01) << 5);
note: if left_adjust is zero ADLAR bit stays at zero.
Program Example:
Here is my Arduino Test Script ported to AVR Studio so I could use the simulator: ArduinoToAVRStudio-Blink ****
In our first example “ADC Subsystem ADMUX Register” we assumed where each field was located within the register. In the next example we do not presuppose the location of the fields. This allows our program to adapt to different microcontroller register definitions. The downside is that, while in the first example the fields could be defined on the fly by the user (for example as arguments to a function), in this next example they must be predefined.
Timer/Counter 2 Control Register A Example 1
In this example, the wave generation mode of Timer/Counter 2 (WGM21:WGM20) is set to Fast PWM (0b11), the compare match output A mode (COM2A1:COM2A0) is configured to set on compare match (0b10), while the configuration bits for output compare register B are not modified (COM2B1:COM2B0).
#define _BV(bit) (1 << (bit))
TCCR2A |= _BV(COM2A1)|_BV(WGM21)|_BV(WGM20); // fast PWM, turn on oc0
Test Your Knowledge 3: At reset TCCR2A is cleared so our C++ example would result in COM2A1:COM2A0 = 0b10. What if another piece of software had set COM2A0 to 1 before this initialization routine was called. In this case our C++ code would not configure the register as expected. How could you solve this problem? Tip: Because of operator precedence (see Compound Operators earlier in this document) you will need to use a simple assignment operator or write two lines of code.
AnswerTCCR2A &= ~_BV(COM2A0)
Timer/Counter 2 Control Register A Example 2
In this example, the wave generation mode of Timer/Counter 2 (WGM21:WGM20) is set to Phase Correct PWM (0b01). The compare match output A mode (COM2A1:COM2A0) is configured to set on compare match, while the configuration bits for output compare register B are not modified (COM2B1:COM2B0). This example makes no assumption about the state of the bits within the WGM or COM2A bit fields.
TCCR2A &= ~_BV(COM2A0) & ~_BV(WGM21); // clear bits
TCCR2A |= _BV(COM2A1) | _BV(WGM20); // set bits
Test Your Knowledge 4: Can you combine these two expressions into one?
AnswerTCCR2A = TCCR2A&~(_BV(WGM21) | _BV(COM2A0)) | _BV(WGM20) | _BV(COM2A1);
Test Your Knowledge 5: If you are allowed to assume the location of the waveform generation mode (WGM2) and the compare match output A mode (COM2A) fields within the TCCR2A register, how would you write an expression that could set these two fields based on user defined variables output_mode and waveform? Hint: see the first example.
AnswerTCCR2A |= (output_mode << 6) | (waveform & 0x03); // analogRead
Test if a Bit is Set or Cleared
source: Special function registers
#define | bit_is_set(sfr, bit) (_SFR_BYTE(sfr) & _BV(bit)) |
#define | bit_is_clear(sfr, bit) (!(_SFR_BYTE(sfr) & _BV(bit))) |
#define | loop_until_bit_is_set(sfr, bit) do { } while (bit_is_clear(sfr, bit)) |
#define | loop_until_bit_is_clear(sfr, bit) do { } while (bit_is_set(sfr, bit)) |
ADC Subsystem ADMUX Register Example
// ADSC is cleared when the conversion finishes
while (bit_is_set(ADCSRA, ADSC));
What is _SFR_BYTE(sfr)?
Source: Playing with Arduino _SFR_BYTE() and PORT
The _SFR_BYTE() is a macro that returns a byte of data of the specified address. The _SFR_BYTE() is defined in hardware/tools/avr/avr/include/avr/sfr_defs.h as below.
#define _SFR_BYTE(sfr) _MMIO_BYTE(_SFR_ADDR(sfr))
The _SFR_ADDR() is a macro that expands the _SFR_MEM_ADDR(sfr) macro. Both are defined in hardware/tools/avr/avr/include/avr/sfr_defs.h as below.
#define _SFR_ADDR(sfr) _SFR_MEM_ADDR(sfr)
The _SFR_MEM_ADDR() is a macro that returns the address of the argument.
#define _SFR_MEM_ADDR(sfr) ((uint16_t) &(sfr))
The _MMIO_BYTE() is a macro that dereferences a byte of data at the specified address. The _MMIO_BYTE() is defined in hardware/tools/avr/avr/include/avr/sfr_defs.h as below. The input is mem_addr and dereferences its contents.
#define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr))
Putting it all together, this is how the compiler would expand _SFR_BYTE(sfr)
_SFR_BYTE(sfr) *(volatile uint8_t * uint16_t &(sfr))
Let’s take a closer look at this expanded macro definition. Starting from the inside and moving out. The ampersand sign (&) is known as a reference operator and lets the compiler know that “sfr” is to be interpreted as an address (i.e., a pointer). Specifically, a 16-bit address (uint16_t).
The asterisk (*) sign in the uint8_t declaration of the pointer does not mean “value pointed by”, it only means that it is a pointer (it is part of its type compound specifier). It should not be confused with the dereference operator, which come next, they are simply two different things represented with the same sign.
The final asterisk sign (*) at the beginning of the statement is a dereference operator. When the dereference operator is used you will get the “value pointed by” a pointer – the actual value of the register.
Mapping the 6-bit I/O address space into the 16-bit extended I/O address space
This works if the “special function register” is in the extended I/O address space but what if it is in the 64 byte I/O address space?
Let’s assume sfr is within the I/O address space of the ATmega microcontroller, for example a GPIO Port. Each GPIO Port includes three registers PINx, DDRx, and PORTx. For the ATmega32U4 “x” would be B, C, D, E, and F
To continue our example let’s assume we are going to write to one of the PORT registers (not to be confused with the PORT itself – see figure above). The PORTB, PORTC and PORTD registers are defined in hardware/tools/avr/avr/include/avr/iom32u4.h as below.
#define PORTB _SFR_IO8(0x05)
#define PORTC _SFR_IO8(0x08)
#define PORTD _SFR_IO8(0x0B)
Again looking at the figure, we see that arguments 0x05, 0x07, and 0x0A are the I/O addresses of PORTB, PORTC and PORTD respectively. They call _SFR_IO8(). The _SFR_IO8() converts the I/O address to the memory address. It is a macro that returns a byte of data at an address of io_addr + __SFR_OFFSET. The _SFR_IO8() is defined in hardware/tools/avr/avr/include/avr/sfr_defs.h as below.
#define _SFR_IO8(io_addr) _MMIO_BYTE((io_addr) + __SFR_OFFSET)
#define __SFR_OFFSET 0x20
Again, _MMIO_BYTE() is a macro that dereferences a byte of data at the specified address.
#define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr))
Putting it all together we have the equivalent macro.
#define _SFR_IO8(io_addr) (*(volatile uint8_t *)(mem_addr)) + 0x20
Appendix
Appendix A: The Arduino Family Tree
The Arduino language (based on Wiring) is implemented in C/C++, and therefore has some differences from the Processing language, which is based on Java.
A Visual Paradigm for Programming – Processing
Source: Wikipedia – Processing (programming language)
- Processing was designed to get non-programmers (originally electronic artists) started with software programming, using a visual context, and to serve as the foundation for electronic sketchbooks.
- The concept of “visual context” makes Processing comparable to Adobe’s ActionScript and Lingo scripting based languages.
- A “sketchbook”, is a minimal alternative to an integrated development environment (IDE)
- Processing is an open source programming language and integrated development environment (IDE)
- The project was initiated in 2001 by Casey Reas and Benjamin Fry, both formerly of the Aesthetics and Computation Group at the MIT Media Lab.
- The language builds on the Java programming language, but uses a simplified syntax and graphics programming model.
From Programming to Microcontrollers – Wiring
- Wiring was design to teach non-programmers (originally electronic artists) how to program microcontrollers.
- Wiring, uses the Processing IDE (sketchbook) together with a simplified version of the C++ programming language (gcc compiler)
- There are now two separate hardware projects, Wiring and Arduino, using the Wiring IDE (sketchbook) and language.
- Fritzing is another software environment of the same sort, which helps designers and artists to document their interactive prototypes and to take the step from physical prototyping to actual product.
From Programming Microcontrollers to the Arduino
Arduino is an open-source electronics prototyping platform based on flexible, easy-to-use hardware and software. It’s intended for artists, designers, hobbyists, and anyone interested in creating interactive objects or environments.
- The C/C++ language is the foundation upon which the Arduino language (like Wiring on which it is based) is built.
- The C/C++ language is implemented using the GCC Compiler and links against the AVR C library AVR Libc and allows the use of any of its functions; see its user manual for details.
- The AVR C Library has its own family tree (nongnu, gcc)
APPENDIX B: Standard Libraries
- EEPROM – reading and writing to “permanent” storage (EEPROM)
- Ethernet – for connecting to the internet using the Arduino Ethernet Shield
- Firmata – for communicating with applications on the computer using a standard serial protocol.
- LiquidCrystal – for controlling liquid crystal displays (LCDs)
- SD – for reading and writing SD cards
- Servo – for controlling servo motors
- SPI – for communicating with devices using the Serial Peripheral Interface (SPI) Bus
- SoftwareSerial – for serial communication on any digital pins
- Stepper – for controlling stepper motors
- Wire – Two Wire Interface (TWI/I2C) for sending and receiving data over a net of devices or sensors.
APPENDIX C: C++ OPERATOR PRECEDENCE
When we start constructing compound assignment statements in order to assign values to fields within a peripheral subsystem register, also known as a special function register (SFR), it is important to remember operator precedence.
Level | Operator | Description | Grouping |
1 | :: | scope | Left-to-right |
2 | () [] . -> ++ — dynamic_cast static_cast reinterpret_cast const_cast typeid | postfix | Left-to-right |
3 | ++ — ~ ! sizeof new delete | unary (prefix) | Right-to-left |
* & | indirection and reference (pointers) | ||
+ – | unary sign operator | ||
4 | (type) | type casting | Right-to-left |
5 | .* ->* | pointer-to-member | Left-to-right |
6 | * / % | multiplicative | Left-to-right |
7 | + – | additive | Left-to-right |
8 | << >> | shift | Left-to-right |
9 | < > <= >= | relational | Left-to-right |
10 | == != | equality | Left-to-right |
11 | & | bitwise AND | Left-to-right |
12 | ^ | bitwise XOR | Left-to-right |
13 | | | bitwise OR | Left-to-right |
14 | && | logical AND | Left-to-right |
15 | || | logical OR | Left-to-right |
16 | ?: | conditional | Right-to-left |
17 | = *= /= %= += -= >>= <<= &= ^= |= | assignment | Right-to-left |
18 | , | comma | Left-to-right |
APPENDIX D: ARDUINO ANALOGREAD FUNCTION
int analogRead(uint8_t pin)
{
uint8_t low, high;
// set the analog reference (high two bits of ADMUX) and select the
// channel (low 4 bits). this also sets ADLAR (left-adjust result)
// to 0 (the default).
ADMUX = (analog_reference << 6) | (pin & 0x0f);
// without a delay, we seem to read from the wrong channel
//delay(1);
// start the conversion
sbi(ADCSRA, ADSC);
// ADSC is cleared when the conversion finishes
while (bit_is_set(ADCSRA, ADSC));
// we hto read ADCL first; doing so locks both ADCL
// and ADave CH until ADCH is read. reading ADCL second would
// cause the results of each conversion to be discarded,
// as ADCL and ADCH would be locked when it completed.
low = ADCL;
high = ADCH;
// combine the two bytes
return (high << 8) | low;
}
APPENDIX E: ADAFRUIT MOTOR SHIELD
Using the Motor Shield
DC Motors
#include
// motor is an instance of the AF_DCMotor class
AF_DCMotor motor(2, MOTOR12_64KHZ); // create motor #2, 64KHz pwm
void setup() {
Serial.begin(9600); // set up Serial library at 9600 bps
Serial.println(“Motor test!”);
motor.setSpeed(200); // set the speed to 200/255
}
void loop() {
Serial.print(“tick”);
motor.run(FORWARD); // turn it on going forward
delay(1000);
Serial.print(“tock”);
motor.run(BACKWARD); // the other way
delay(1000);
Serial.print(“tack”);
motor.run(RELEASE); // stopped
delay(1000);
}
Stepper Motor
#include
// motor is an instance of the AF_Stepper class
AF_Stepper motor(48, 2);
void setup() {
Serial.begin(9600); // set up Serial library at 9600 bps
Serial.println(“Stepper test!”);
motor.setSpeed(10); // 10 rpm
motor.step(100, FORWARD, SINGLE);
motor.release();
delay(1000);
}
void loop() {
motor.step(100, FORWARD, SINGLE);
motor.step(100, BACKWARD, SINGLE);
motor.step(100, FORWARD, DOUBLE);
motor.step(100, BACKWARD, DOUBLE);
motor.step(100, FORWARD, INTERLEAVE);
motor.step(100, BACKWARD, INTERLEAVE);
motor.step(100, FORWARD, MICROSTEP);
motor.step(100, BACKWARD, MICROSTEP);
}
Library Header
What is a Library?
// Adafruit Motor shield library
// copyright Adafruit Industries LLC, 2009
// this code is public domain, enjoy!
#ifndef _AFMotor_h_
#define _AFMotor_h_
#include
#include
//#define MOTORDEBUG 1
#define MICROSTEPS 16 // 8 or 16
#define MOTOR12_64KHZ _BV(CS20) // no prescale
#define MOTOR12_8KHZ _BV(CS21) // divide by 8
#define MOTOR12_2KHZ _BV(CS21) | _BV(CS20) // divide by 32
#define MOTOR12_1KHZ _BV(CS22) // divide by 64
#define MOTOR34_64KHZ _BV(CS00) // no prescale
#define MOTOR34_8KHZ _BV(CS01) // divide by 8
#define MOTOR34_1KHZ _BV(CS01) | _BV(CS00) // divide by 64
#define MOTOR1_A 2
#define MOTOR1_B 3
#define MOTOR2_A 1
#define MOTOR2_B 4
#define MOTOR4_A 0
#define MOTOR4_B 6
#define MOTOR3_A 5
#define MOTOR3_B 7
#define FORWARD 1
#define BACKWARD 2
#define BRAKE 3
#define RELEASE 4
#define SINGLE 1
#define DOUBLE 2
#define INTERLEAVE 3
#define MICROSTEP 4
// Arduino pin names
#define MOTORLATCH 12
#define MOTORCLK 4
#define MOTORENABLE 7
#define MOTORDATA 8
class AFMotorController
{
public:
AFMotorController(void);
void enable(void);
friend class AF_DCMotor;
void latch_tx(void);
};
class AF_DCMotor
{
public:
AF_DCMotor(uint8_t motornum, uint8_t freq = MOTOR34_8KHZ);
void run(uint8_t);
void setSpeed(uint8_t);
private:
uint8_t motornum, pwmfreq;
};
class AF_Stepper {
public:
AF_Stepper(uint16_t, uint8_t);
void step(uint16_t steps, uint8_t dir, uint8_t style = SINGLE);
void setSpeed(uint16_t);
uint8_t onestep(uint8_t dir, uint8_t style);
void release(void);
uint16_t revsteps; // # steps per revolution
uint8_t steppernum;
uint32_t usperstep, steppingcounter;
private:
uint8_t currentstep;
};
uint8_t getlatchstate(void);
#endif
Adafruit Private Functions
latch_tx
#define _BV(bit) (1 << (bit))
/*
Send data located in 8-bit variable latch_state
to the 74HC595 on the Motor Shield.
*/
void AFMotorController::latch_tx(void) {
uint8_t i;
//LATCH_PORT &= ~_BV(LATCH);
digitalWrite(MOTORLATCH, LOW); // – Output register clock low
//SER_PORT &= ~_BV(SER);
digitalWrite(MOTORDATA, LOW); // – Serial data bit = 0
for (i=0; i<8; i++) { // – Shift out 8-bits
//CLK_PORT &= ~_BV(CLK);
digitalWrite(MOTORCLK, LOW); // – Shift clock low
if (latch_state & _BV(7-i)) { // – Is current bit of
//SER_PORT |= _BV(SER); latch_state == 1
digitalWrite(MOTORDATA, HIGH); // – Yes, serial data bit = 1
} else {
//SER_PORT &= ~_BV(SER);
digitalWrite(MOTORDATA, LOW); // – No, serial data bit = 0
}
//CLK_PORT |= _BV(CLK);
digitalWrite(MOTORCLK, HIGH); // – Shift clock high, rising edge
} // shift bit into shift register
//LATCH_PORT |= _BV(LATCH);
digitalWrite(MOTORLATCH, HIGH); // – Output register clock high, rising
} // edge sends the stored bits to the
// output register.
enable
/*
Configure DDR Registers B and D bits assigned to
the input of the 74HC595 on the Motor Shield. Output
all zeros and enable outputs.
*/
void AFMotorController::enable(void) {
// setup the latch
/*
LATCH_DDR |= _BV(LATCH);
ENABLE_DDR |= _BV(ENABLE);
CLK_DDR |= _BV(CLK);
SER_DDR |= _BV(SER);
*/
pinMode(MOTORLATCH, OUTPUT);
pinMode(MOTORENABLE, OUTPUT);
pinMode(MOTORDATA, OUTPUT);
pinMode(MOTORCLK, OUTPUT);
latch_state = 0;
latch_tx(); // “reset”
//ENABLE_PORT &= ~_BV(ENABLE); // enable the chip outputs!
digitalWrite(MOTORENABLE, LOW);
}
Adafruit Motor Public Functions
run
void AF_DCMotor::run(uint8_t cmd) {
uint8_t a, b;
/* Section 1: choose two shift register outputs based on which
* motor this instance is associated with. motornum is the
* motor number that was passed to this instance’s constructor.
*/
switch (motornum) {
case 1:
a = MOTOR1_A; b = MOTOR1_B; break;
case 2:
a = MOTOR2_A; b = MOTOR2_B; break;
case 3:
a = MOTOR3_A; b = MOTOR3_B; break;
case 4:
a = MOTOR4_A; b = MOTOR4_B; break;
default:
return;
}
/* Section 2: set the selected shift register outputs to high/low,
* low/high, or low/low depending on the command. This is done
* by updating the appropriate bits of latch_state and then
* calling tx_latch() to send latch_state to the chip.
*/
switch (cmd) {
case FORWARD: // high/low
latch_state |= _BV(a);
latch_state &= ~_BV(b);
MC.latch_tx();
break;
case BACKWARD: // low/high
latch_state &= ~_BV(a);
latch_state |= _BV(b);
MC.latch_tx();
break;
case RELEASE: // low/low
latch_state &= ~_BV(a);
latch_state &= ~_BV(b);
MC.latch_tx();
break;
}
}
setSpeed
void AF_DCMotor::setSpeed(uint8_t speed) {
switch (motornum) {
case 1:
OCR2A = speed; break;
case 2:
OCR2B = speed; break;
case 3:
OCR0A = speed; break;
case 4:
OCR0B = speed; break;
}
}
initPWM1
This is a brief excerpt of the AF_DCMotor constructor (subroutine initPWM1(freq)), which is initializing speed control for motor 1:
// use PWM from timer2A
TCCR2A |= _BV(COM2A1) | _BV(WGM20) | _BV(WGM21); // fast PWM, turn on oc0
TCCR2B = freq & 0x7;
OCR2A = 0;
DDRB |= _BV(3);
break;
General Purpose I/O Ports
Table of Contents
Source: ATmega328P Data Sheet http://www.atmel.com/dyn/resources/prod_documents/8161S.pdf page 5
ATMEGA GENERAL PURPOSE DIGITAL I/O PORTS
Reading: Section 6.1.1 Introduction
- The ATmega32U4 has 26 General Purpose Digital I/O Pins assigned to 5 Ports (8-bit Ports B, D, 2-bit Port C,E, and 6-bit Port F)
- Each I/O port pin may be configured as an output with symmetrical drive characteristics. Each pin driver is strong enough (20 mA) to drive LED displays directly.
- Each I/O port pin may be configured as an input with or without a pull-up resistor. The values for the pull up resistor can range from 20 – 50 K ohms.
- Each I/O pin has protection diodes to both VCC and Ground
PIN DESCRIPTION OF THE ATMEGA32U4
Reading: Section 6.1.3 Pin-Muxing
I/O Ports B, C, D, E, and F
Ports B (PB7..PB0), C (PC7,PC6), D (PD7..PD0), E (PE6,PE2), F (PF7..PF4,PF1,PF0) are bi-directional I/O ports with internal pull-up resistors (selected for each bit). The Port output buffers have symmetrical drive characteristics with both high sink and source capability.
Interrupts
External Interrupts are triggered by the INT0 to INT3 and INT6 pins or any of the PCINT7..0 pins associated with Port B. Observe that, if enabled, the interrupts will trigger even if the pins are configured as outputs. This feature provides a way of generating a software interrupt.
Analog Voltage (AVCC)
AVCC is the supply voltage pin for the A/D Converter. It should be externally connected to VCC. If the ADC is used, it should be connected to VCC through a low-pass filter.
Analog Reference (AREF)
AREF is the analog reference pin for the A/D Converter.
Analog to Digital Converter (ADC)
The ATmega32U4 includes a 10-bit ADC. The ADC is connected to a analog multiplexer with six single-ended voltage input (ADC0, ADC1, ADC4 to ADC7) channels. The device also supports differential inputs, with the positive input terminal selectable between ADC4 to ADC7, and negative input terminal selectable between ADC0 and ADC1. The output of the differential amplifier goes to a gain stage, with four (4) programmable amplification steps of 0 dB (1x), 10 dB (10x), 16dB (40x), and 23dB (200x). If 1x, 10x, or 40x gain is used, 8-bit resolution can be expected. If 200x gain is used, 7-bit resolution can be expected.
I/O PORT PIN AS AN OUTPUT
Reading: Section 6.1.2 Basic Operation
- To configure a Port (x) pin as an output set corresponding bit (n) in the Data Direction Register (DDxn) to 1. Once configured as an output pin, you control the state of the pin (1 or 0) by writing to the corresponding bit (n) of the PORTxn
- Writing (signal WPx) a logic one to PINxn toggles the value of PORTxn, independent on the value of DDxn. Note that the SBI instruction can be used to toggle one single bit in a port.
I/O PORT PIN AS AN INPUT
Reading: Section 6.2 Input
- To configure a Port (x) pin as an input set corresponding bit (n) in the Data Direction Register (DDxn) to 0. To add a pull-up resistor set the corresponding bit (n) of the PORTxn register to 1 (see illustration).
- You can now read the state of the input pin by reading the corresponding bit (n) of the PINxn
ACCESSING GPIO LINES IN ASSEMBLY and C++
DESIGN EXAMPLE 1 – Initialize 3DoT
Problem: When power is applied to the ATmega32U4 on the 3DoT board, it needs to be configured. However, because the 3DoT board, like the Arduino UNO, is itself integrated into another system, it is not completely aware of its environment. Therefore, pins allocated to the shields should not be modified.
The following tables show how the code should configure Ports B (PB7..PB0), C (PC7,PC6), D (PD7..PD0), E (PE6,PE2), F (PF7..PF4,PF1,PF0).
Port B
PIN | PB7 | PB6 | PB5 | PB4 | PB3 | PB2 | PB1 | PB0 |
J5-1 | J5-2 | J5-3 | J5-4 | |||||
NET | AIN1 | BIN2 | NC | NC | MISO | MOSI | SCK | SS |
DDRB | 1 | 1 | 0 | 0 | X | X | X | X |
PORTB | 0 | 0 | 1 | 1 | X | X | X | X |
/* undefind pins are not changed */ /* pins that are not connected are defined as inputs with pull-up resistors */ DDRB &= 0x0F; // clear most significant nibble DDRB |= 0xC0; // set bit pattern PORTB &= 0x0F; // clear most significant nibble PORTB |= 0x30; // set bit pattern
Port C
PIN | PC7 | PC6 | PC5 | PC4 | PC3 | PC2 | PC1 | PC0 |
NET | BIN1 | NC | – | – | – | – | – | – |
DDRC | 1 | 0 | – | – | – | – | – | – |
PORTC | 0 | 1 | – | – | – | – | – | – |
DDRC &= 03F; // clear most significant 2 bits DDRC |= 0x80; // set bit pattern PORTC &= 03F; // clear most significant 2 bits PORTC |= 0x40;
Port D
PIN | PD7 | PD6 | PD5 | PD4 | PD3 | PD2 | PD1 | PD0 |
Servo A | LED | Servo B | J1-5 | J1-4 | J3-3 | J3-2 | ||
NET | AIN2 | SIG | BUILTIN | SIG | TX | RX | SDA | SCL |
DDRD | 1 | 1 | 1 | 1 | X | X | X | X |
PORTD | 0 | 0 | 0 | 0 | X | X | X | X |
DDRD |= 0xF0; // set most significant nibble PORTD &= 0x0F; // clear most significant nibble
Port E
PIN | PE7 | PE6 | PE5 | PE4 | PE3 | PE2 | PE1 | PE0 |
S-1 | ||||||||
NET | – | NC | – | – | – | SW | – | – |
DDRE | – | 0 | – | – | – | 0 | – | – |
PORTE | – | 1 | – | – | – | 0 | – | – |
DDRE &= 0xBB; // clear bits 6 and 2 PORTE &= 0xBB; // clear bits 6 and 2 PORTE |= 0x40; // set bit pattern
Port F
PIN | PF7 | PF6 | PF5 | PF4 | PF3 | PF2 | PF1 | PF0 |
J2-5 | J2-6 | J2-7 | J2-8 | J2-6 | ||||
NET | A0 | A1 | A2 | A3 | – | – | A4 | BATT |
DDRF | X | X | X | X | – | – | X | 0 |
PORTF | X | X | X | X | – | – | X | 0 |
DDRF &= 0xFE; PORTF &= 0xFE;
DESIGN EXAMPLE 2 – CSULB Shield Read Switches
Arduino Script
// Analog pins0,1,2,3,4,5 map to Digital pins 14, 15, 16, 17, 18, 19 pinMode(14, INPUT_PULLUP); // set pin to input with pullup resistor
Repeat this 1 line for pins 15, 16, 17, 18, and 19.
C++
DDRC &= ~0b00111111; PORTC |= 0b00111111;
Assembly
; Initialize Switches with Pull-up resistors in r16, DDRC // Port C DDR for switches 5 to 0 cbr r16,0b00111111 // define bits 5 to 0 as input (clear) out DDRC,r16 // output DDxn = 0 PORTxn = Undefined in r16,PORTC // PORT C Register for switches 5 to 0 sbr r16,0b00111111 // add pull-up resistors (PUR) out PORTC,r16 // output DDxn = 0 PORTxn = 1 Main: : in r7,0x06 // R7 ← PINC bst r7,4 // T ← R7 bit 4 bld r8,0 // R8 bit 0 (seg_a) ← T
DESIGN EXAMPLE 3 – CSULB Shield Configure D Flip-Flop
Arduino Script
// Variable definitions const int dff_clk = 5; // D flip-flop clock wired to digital pin 5 const int dff_Q = 2; // D flip-flop Q output wired to digital pin 2 // Initialize digital pin 5 (dff_clk) as an // output and digital pin 2 (dff_Q) as an input pinMode(dff_clk, OUTPUT); // set pin as output digitalWrite(dff_clk, LOW); // initialize to zero pinMode(dff_Q, INPUT); // set pin as input
C++
// Preprocessor directives #define dff_clk PORTD5 #define dff_Q PIND2 // Initialize push-button debounce circuit DDRD |= 1 << dff_clk; // define bit 5 of Data Direction Register //(DDR)PORT D as an output PORTD &= ~(1 << dff_clk); // initialize to zero DDRD &= ~(1 << dff_Q); // define bit 2 of Data Direction Register //(DDR)PORT D as an input PORTD &= ~(1 << dff_Q); // without a pull-up resistor
Assembly
; Assembly directives .EQU dff_clk=PORTD5 .EQU dff_Q=PIND2 ; Initialize push-button debounce circuit sbi DDRD, dff_clk // flip-flop clock, DDRD5 = 1 PORTD5 = Undefined cbi PORTD, dff_clk // DDRD5 = 1 PORTD5 = 0 cbi DDRD, dff_Q // flip-flop Q DDRD2 = 0 PORTD2 = Undefined cbi PORTD, dff_Q // DDRD2 = 0 PORTD2 = 0
ATmega32U4 Register Summary and the I/O Port
- Three I/O memory address locations are allocated for each port, one each for the Data Register – PORTx, Data Direction Register – DDRx, and the Port Input Pins – PINx.
- The Port Input Pins I/O location PINx is Read Only, while the Data Register and the Data Direction Register are read/write.
- However, Writing a logic one to a bit in the PINx Register, will result in a Toggle in the corresponding bit in the Data Register.
- In addition, the Pull-up Disable – PUD bit in MCUCR disables the pull-up function for all pins in all ports when set.
I/O PORT PIN SCHEMATIC
I/O PORT PIN CONFIGURATIONS
Appendix A – PROGRAM I/O PORT AS AN INPUT USING MNEMONICS
In the original Read Switches example, I programmed GPIO Port C bits 5 to 0 as inputs with pull-up resistors. Read GPIO Port C into register r7 and moved bit 4 to register r8 bit 0. In addition the program did not modify Port C bits 7 and 6. In this version, mnemonics and the shift operator are used to increase clarity (i.e., make the code more self documenting).
Appendix B – I/O PORT PIN “SYNCHRONIZER”
- As previously discussed, you read a port pin by reading the corresponding PINxn Register bit. The PINxn Register bit and the preceding latch constitute a synchronizer. This is needed to avoid metastability if the physical pin changes value near the edge of the internal clock, but it also introduces a delay as shown in the timing diagram.
- Consider the clock period starting shortly after the first falling edge of the system clock. The latch is closed when the clock is low, and goes transparent when the clock is high, as indicated by the shaded region of the “SYNC LATCH” signal. The signal value is latched when the system clock goes low. It is clocked into the PINxn Register at the succeeding positive clock edge. As indicated by the two arrows tpd,max and tpd,min, a single signal transition on the pin will be delayed between ½ and 1½ system clock period depending upon the time of assertion.
Appendix C – SWITCHING BETWEEN I/O PORT PIN CONFIGURATIONS
- When switching between tri-state ({DDxn, PORTxn} = 0b00) and output high ({DDxn, PORTxn} = 0b11), an intermediate state with either pull-up enabled ({DDxn, PORTxn} = 0b01) or output low ({DDxn, PORTxn} = 0b10) must occur.
- Switching between input with pull-up ({DDxn, PORTxn} = 0b01) and output low ({DDxn, PORTxn} = 0b10) generates the same problem. You must use either the tri-state ({DDxn, PORTxn} = 0b00) or the output high state ({DDxn, PORTxn} = 0b11) as an intermediate step.
PWM Motor Speed Control using Timer/Counter 4
This article is on the motor control section of the 3DoT board using Timer/Counter 4 operating in Fast PWM mode.
For the remainder of this article use Figure 1 “Atmega32U4 to Motor Driver Interface” to help you cross-reference the tower of babel names used by Atmel, Arduino, and Toshiba (i.e., TB6612FNG).
The direction of motors A and B are defined by inputs AIN1, AIN2, and BIN1, BIN2.
The speed of the motors A and B are controlled by changing the duty cycle of pins PWMA and PWMB respectively. With reference to Figure 1 “ATmega32U4 to Motor Driver Interface,” the speed of motor A will be controlled by Timer 4 register OC4D and motor B by Timer 4 register OC4B. The mnemonic OCnx stands for Output Compare register nx, where n is the Timer number (0, 1, 3, and 4) and x is the Compare register (Timer 4 has four (4) output compare registers designated A, B, C, and D). We will be operating our timer using “Fast Pulse Width Modulation.” I will tell you more about these registers and modes in the coming sections.
Table of Contents
Reference(s):
- The AVR Microcontroller and Embedded Systems using Assembly and C, by Muhammad Ali Mazidi, Sarmad Naimi, and Sepehr Naimi
Chapter 16: PWM Programming and DC Motor Control in AVR - ATMEL 8-bit AVR Microcontroller with 16/32K Bytes of ISP Flash and USB – ATmega32U4
Chapter 13 “8‑bit Timer/Counter 0 with PWM,” Chapter 14 “16-bit Timers/Counters,” and Chapter 15 “10-bit High Speed Timer/Counter4.”
Motor Direction Control
Figure 1 “Atmega32U4 to Motor Driver Interface” shows that we can configure 2 motors where each motor has 3 control pins and a standby (STBY) pin for the entire IC. If STBY is set low then the motors are not powered regardless of the state of the other two pins. The pins attached to AIN1/2 and BIN1/2 are digital outputs that control the rotation of the motor. The PWM inputs are connect to a pins capable of a PWM output to control the speed of the motor.
To prevent damage to the internal circuitry of the TB6612FNG, the IC includes clamping diodes on the inputs, a series resistor to limit in-rush current, and a weak pull-down resistor to keep the N-channel MOSFET OFF (Figure 2a Input Circuit). To prevent damage to the output circuitry internal flyback diodes are included (Figure 2b Flyback Diodes). Here is one of many articles on how to use a MOSFET as a Switch which goes into a little more details on these circuit elements.
The direction in which the motors turn are defined in Table 1.
To turn clockwise (CW) you would want to set the H-Bridge to state t1 (see Figure 3 state t1). This would be accomplished by setting PD6 (AIN1) = 1 and PD4 (AIN2) = 0.
PORTD |= _BV(PD6) ; PORTD &= ~(_BV(PD4));
For a simple “toy” brushed DC motor the magnetic field of the rotor is generated by copper wire wound around a steel-laminate core. The magnetic field is switched by a commutator located on the shaft. The stator uses fixed magnets. The rotor’s copper winding from an electrical perspective is an inductor. During normal operation the back emf generated by the rotor’s winding (an inductor) as it is switched off is routed through a flyback diode (aka flywheel, free wheeling, snubber, suppresser, catch, clamp, kick back).
When switching the motor from CW to CCW care must be taken to not short the MOSFET switches on both legs of the H-bridge while also dissipating the current generated by the collapsing magnetic fields. To solve this problem a Finite State Machine (FSM) is implemented. The controlled transition from one state to the next of the FSM is illustrated in Figure 2 “TB6612FNG H-Bridge states t1 to t5” and Figure 4 “How to program switching between states to prevent short circuit conditions.”
These figures are taken from the TB6612FNG datasheet, and it is the author’s belief that the Toshiba editors made a copy-paste error for states t4 and t5 in Figure 3.
Sample C++ Code to Configure GPIO Ports
Figure 1 is duplicated here for reference purposes.
// MOTOR PINS // Motor A AIN1,AIN2,PWMA = PD6,4,7 DDRD |= _BV(PD7) | _BV(PD6) | _BV(PD4); // 0xD0 // Default to low output PORTD &= ~(_BV(PD7) | _BV(PD6) | _BV(PD4)); // Motor B BIN1,PWMB = PB5,6 and STBY = PB4 DDRB |= _BV(PB6) | _BV(PB5) | _BV(PD4); //0x70 PORTB &= ~(_BV(PB6) | _BV(PB5 | _BV(PD4))); // and for motor B BIN2 = PC6 DDRC |= _BV(PC6) ; //0x40 PORTC &= ~(_BV(PC6));
Motor Speed Control
Several modulation methods have been developed for applications that require a digital representation of an analog signal. One popular and relevant scheme is pulse width modulation (PWM) in which the instantaneous amplitude of an analog signal is represented by the width of periodic rectangular wave. For example, consider the signals depicted in the figure below. Notice, the PWM version of the signal has a fixed frequency defining the point when a pulse begins. During the period of an individual pulse, the signal remains high for an amount of time proportional to the amplitude of the analog signal.
The speed of our DC motors is controlled using pulse-width-modulation (PWM). But, unlike the sinewave above, where the duty cycle changes after each cycle, we will be controlling the speed of our motors by setting a fixed duty cycle for a given speed. When the rectangular wave signal is high the motor is powered ON, and when the signal is low the power is turned OFF. The speed of the motor is controlled by the fraction of time the controlling signal is ON (duty cycle = Th/Tp %, where Th = time high and Tp = clock period).
ATmega Timer Modes
ATmega, Arduino, and 3DoT PWM Output Pins
Most microcontrollers, including the ATmega family of microcontrollers, provide at least one port that has timer sub-circuitry capable of generating PWM signals on a port pin. Typically, one just needs to configure the frequency and desired duty cycle via a couple of registers. When enabled, the port pin will output a PWM signal that can be demodulated in order to provide an approximation to an analog signal. In our design the characteristics of the motor circuit act to demodulate the PWM signal.
The Arduino Leonardo, on which the 3DoT board is based, can generate rectangular waves for PWM on digital pins 3, 5, 6, 9, 10, 11, 13. The 3DoT supports PWM signals on digital pins 3, 6, 10, 11, and 13.
ATmega32U4 Timing Subsystem
The ATmega32U4 processor has 4 timer/counter (TC) modules that can be used to generate a PWM signal. They are numbered 0, 1, 3, and 4. I will use TCx convention from now on.
Timer/Counter0 is an 8-bit Timer/Counter module, with two independent Output Compare Units, and with PWM support. The Arduino uses Timer 0 to implement the delay(), millis(), and Servo library functions.
Timer/Counter1 and Timer/Counter3 are 16-bit Timer/Counter units with three independent double-buffered Output Compare Units.
Timer/Counter4 is the only 10-bit high speed timer on the ATmega32U4 and has a lot of advanced features, including a high precision mode, double buffering (no glitches), dead time (break before make), fault protection with noise canceling (motor stall monitoring), and even support for brushless dc motors. To keep things simple we will not be using any of these features.
Looking at Figure 1 “ATmega32U4 to Motor Driver Interface” again we see the PWMA and PWMB are associated with OC4D and OC4B respectively. OC4X denotes an output compare with TCNT4. The ATmega32U4 has a more extensive timer system than the ATmega328P and this timer (TCNT4) has 4 Output Compare Registers OCRs attached to it. More on this subject shortly.
What is Fast Pulse Width Modulation
The timing diagram for the fast PWM mode is shown in Figure 15-3. The counter is incremented until the counter value matches the TOP value. The counter is then cleared at the following timer clock cycle. The TCNTn value is in the timing diagram shown as a histogram for illustrating the single-slope operation. The diagram includes the Waveform Output in non-inverted and inverted Compare Output modes. The small horizontal line marks on the TCNTn slopes represent Compare Matches between OCRnx and TCNTx. Figure 15-3 is true for Timer/Counter 4 operation. The only difference for Timer/Counters 0, 1, and 3, is the mnemonic OCWnx, which is replaced simply by OCnx.
The Timer/Counter Overflow Flag (TOVn bit) is set each time the counter reaches TOP. In fast PWM mode, the compare unit allows generation of PWM waveforms on the OCnx pins. In our case OC4D for Motor A and OC4B for Motor B.
Table 15-1 Definitions
For Timer/Counter 4 the OCR4C holds the Timer/Counter TOP value, i.e. the clear on compare match value. The Timer/Counter4 High Byte Register (TC4H) is a 2-bit register[1] that is used as a common temporary buffer to access the MSB bits of the Timer/Counter4 registers, if the 10-bit accuracy is used (Section 15.2.3 Registers). |
[1] Enhanced PWM mode adds an additional 3rd bit to the TC4H register.
PWM Output Frequency
Timer/Counter 4
For Timer/Counter 4 the PWM output frequency can be calculated by the following equation (Section 14.8.3).
I believe the equation defined in Section 15.8.2 “Fast PWM Mode” is incorrect and have replaced with equation used for calculating the frequency throughout the rest of the document.
Equation 1.0 |
The frequency fOC4X as defined by equation 1 is a function of the system clock (8 MHz), the Prescaler, and TOP. The N variable represents the prescale divider and is defined in TCCR4B CS43:CS40 (stopped, 1, 2, 4,…16384). For our design solutions, N = 1 (no Prescaler) with the 2 bits in TC4H set to zero.,
If we want to replicate the Arduino default frequency of 31.25 kHz, then we would want to keep our TOP, defined by OCR4C, at its default value of 0xFF.
fOC4X = fCLK / 256 = 8 MHz / 256 = 31.25 KHz (approximately 32 kHz)
If we want to replicate the Adafruit Motor Shield version 1 frequency of 64kHz (62.5kHz), then we would set TOP to 0x7F.
fOC4X = fCLK / 128 = 8 MHz / 128 = 62.5 KHz (approximately 64 kHz)
If we really want 64kHz set TOP to 0x7C
fOC4X = fCLK / (1+124) = 8 MHz / 125 = 64 kHz
- At the highest range, the DRV8848 Dual H-Bridge Motor Driver does not support frequencies greater than 250kHz. Another popular motor driver the TB6612FNG does not support frequencies greater than 100kHz.
- At the lowest range, the Adafruit Motor Shield v2 operates at a frequency of 1.6 kHz, due to the limitations of the PCA9865 PWM chip. This frequency is well within the audible range and not recommended.
Calculating the PWM Duty Cycle
We will be operating our 10-bit timer/counter 4 as an 8-bit timer in Fast PWM mode.
- Frequency set by OC4C (TOP) will be defined as 0xFF = 25510
- The most significant 2-bits contained in register TC4H will always be zero.
- TCNT4 will be compared to OCR4D (Motor A) and OCR4B (Motor B).
- Therefore, the Duty Cycle = OCR4x/255, where x equals D or B
10-bit Timer/Counter 4 Register
Timer 4 is a 10-bit timer/counter. Special considerations need to be taken when writing to or reading from a 10-bit register. To write to a 10-bit register, write the most significant 2 bits to TC4H first, followed by the least significant byte (for example TCNT4). The TC4 register is shared by all 10-bit registers in Timer/Counter 4. One consequence of this common register, is that when you read a 10-bit register, the most significant 2-bits are saved to TC4H. Consequently, any subsequent 8-bit write operation to the least significant byte of a 10-bit register, will have this new TC4H value written to the high order bits. Again, this potentially unintended consequence can be avoided by always writing to TC4H first. For more on working with a 10-bit register read Atmel Document 7766 “8-bit AVR Microcontroller with16/32K Bytes of ISP Flash and USB Controller,” Section 15.11 “Accessing 10-bit Register.”
For our robots, the good news is that we never read a 10-bit register. Specifically, the Timer/Counter4 high byte (TC4H) will be always be kept at its default value of zero (0x00).
If you were wondering, TC410 (Bit 2) is an “optional” accuracy bit for 11-bit accesses in Enhanced PWM mode. The enhanced PWM mode allows you to get one more accuracy bit while keeping the frequency identical to normal mode. For more information on this topic see Section 15.6.2 “Enhanced Compare/PWM mode Timer/Counter 4” in the ATmega32U4 Datasheet.
TC4H – Timer/Counter4 High Byte (0xBF)
TCNT4 – Timer/Counter4 (0xBE)
Although Timer 4 is a 10-bit timer/counter, we will be operating it as an 8-bit timer.
Configuring Timing/Counter 4
For our fast PWM implementation we need:
- To enable the Fast PWM Mode
- define output waveform shape
- set an appropriate timer frequency and…
- duty cycle by configuring the OCR registers
Step 1 – Enable Fast PWM mode
Step 2 – Define output waveform shape
PWM Waveform Generation Modes
Four types of pulse-width modulation (PWM) are possible:
1. The trailing edge can be fixed and the lead edge modulated. ATmega32U4 Timer 4 Fast PWM (PWM4x=1:WGM41..40=00).
2. The leading edge can be fixed and the tail edge modulated. Not implemented on ATmega32U4.
3. The pulse center may be fixed in the center of the time window and both edges of the pulse moved to compress or expand the width. ATmega32U4 Timer 4 PWM Phase and Frequency Correct mode (PWM4x=1:WGM41..40=01)
4. The frequency can be varied by the signal, and the pulse width can be constant. While ATmega32U4 Timers 0,1, and 3 support the CTC mode, Timer 4 does not. Here is an article that shows you how to blink an LED using the CTC Mode (plus how to toggle a bit in C++). In place of the CTC mode, Timer 4 supports two new timer modes, PWM6 / Single slope, and PWM 6 / Dual-slope. These two modes are unique to Timer 4 and are designed for brushless DC motor control.
Timer/Counter4 Control Register A and C – Comparator Output Mode bits
To simplify the definition of the Comparator Output Mode bits located in TCCRA and TCCR4C, I am going to be define COM4D1:0 in TCCR4A. The discussion is directly applicable to the definition of COM4B1:COM4B0 in TCCR4C.
Comparator D Output Mode (COM4D1:COM4D0) TCCR4A bits 3 and 2, control the behavior of the Waveform Output (OCW4D) and the connection of the Output Compare pin (OC4D). If one or both of the COM4D1:0 bits are set, the OC4D output overrides the normal port functionality of the I/O pin it is connected to. The complementary OC4D output is connected only in PWM modes when the COM4D1:0 bits are set to “01”. Note that the Data Direction Register (DDR) bit corresponding to the OC4D pin must be set in order to enable the output driver. The function of the COM4D1:0 bits depends on the PWM4D and WGM40 bit settings. Table 15-17 shows the COM4D1:0 bit functionality when the PWM4D bit is set to a Fast PWM Mode.
Timer/Counter4 Control Register A and C: C++ Code Example
7654_3210 TCCR4C = 0b0000_1001 = 0x09; TCCR4A = 0b0010_0001 = 0x21; //Setting COMD and PWM4D TCCR4C |= (_BV(COM4D1)| _BV(PWM4D)); TCCR4C &= ~(_BV(COM4D0)); TCCR4A |= (_BV(COM4B1)| _BV(PWM4B)); TCCR4A &= ~(_BV(COM4B1));
Step 3 –Set an appropriate timer frequency
Step 4 –Set the duty cycle by configuring the OCR registers
Calculating the PWM Duty Cycle
As illustrated in Figure 15-4, the 8-bit Timer/Counter Output Compare Registers OCR4x (where x = B or D) are compared with Timer/Counter4. On compare match the OC4x pin is cleared to 0 (see Table 15-17 Compare Output Mode, Fast PWM Mode). Write to this register to set the duty cycle of the output waveform. A compare match will also set the compare interrupt flag OCF4B after a synchronization delay following the compare event.
The Duty Cycle = OCR4x/255, where x equals D or B
OCR4B – Timer/Counter4 Output Compare Register D (Section 15.12.9)
OCR4D – Timer/Counter4 Output Compare Register D (Section 15.12.11)
Timer/Counter4 Output Compare Register: C++ Code Example
// Send argument 'A' for motor A, otherwise motor B selected. void setPWM(char pin, uint8_t val){ (pin == 'A') ? (OCR4D = val) : (OCR4B = val) ; // Ternary solution (untested) }
Fast PWM C++ Code Example
Handling PWM in C++ will be divided into two functions(or sections).
- Configure the Timer and OCR
- Set OCR value as needed to change PWM
void initRobotPWM(){ // Configure Motor GPIO Port Pins // Motor A PD6,4,7 DDRD |= _BV(PD7) | _BV(PD6) | _BV(PD4); // 0xD0 // Default to Low output PORTD &= ~(_BV(PD7) | _BV(PD6) | _BV(PD4)); // Motor B PB5,6 and STBY = PB4 DDRB |= _BV(PB6) | _BV(PB5) | _BV(PD4); //0x70 PORTB &= ~(_BV(PB6) | _BV(PB5 | _BV(PD4))); // and for motor B PC6 BIN2 DDRC |= _BV(PB6) ; //0x40 PORTC &= ~(_BV(PB6)); // Configure Timer4 for PWMs // Motor A on PD7 (OC4D) // Motor B on PB6 (OC4B) // Ignore 10-bit mode for ease of use // Need to configure Timer4 for fast PWM // PWM4D and PWM4B set with WGM4 1:0 = 0b00 // Setting WGM = 00 TCCR4D &= ~(_BV(WGM41) | _BV(WGM40)); // Set PD7 and PB6 as outputs // I have also added digital pins since they are part of the same system // If I want the PWM then I want the digitals also // Setting PWM4B and COMB TCCR4A |= (_BV(COM4B1)| _BV(PWM4B)); TCCR4A &= ~(_BV(COM4B1)); // Setting PWM4D and COMD TCCR4C |= (_BV(COM4D1)| _BV(PWM4D)); TCCR4C &= ~(_BV(COM4D0)); // SetPrescaler - turn on timer // Assumes *Mhz external with default fuses (making Fio = 1Mhz) // TB66612FNG says wants PWM Freq <= 100k OCR4C = 0xFF; // CS4 3:0 = 0b0001; TCCR4B |= _BV(CS40); TCCR4B &= ~(_BV(CS43)| _BV(CS42)|_BV(CS41)); // Clear prior settings from Arduino. }
With the configuration ready we can make a function to set our PWM by changing the OCR threshold.
// Send argument 'A' for motor A, otherwise motor B selected. void setPWM(char pin, uint8_t val){ (pin == 'A' ) ? (OCR4D = val) : (OCR4B = val) ; // Ternary solution (untested) }
Fast PWM Assembly Code Example
The following code example shows how to configure timer/counter 4 for Fast PWM operation, at a frequency of 31.25 KHz.
Reset: /* Test code for motor A */ clr r0 // r0 = 0x00 clr r1 com r1 // r1 = OxFF /* Test code for motors A and B */ cbi PORTD, 7 // outputs 0 to Motor A PWM pin when timer/counter 4 disconnected cbi PORTB, 6 // outputs 0 to Motor B PWM pin when timer/counter 4 disconnected // see section 15.11 Accessing 10-bit Register sts TC4H, r0 // most significant 2-bits sts TCNT4, r0 // 10-bit write TC4H:TCNT4 = 0x000 // frequency = (8MHz/prescaler)/OCR4C = 31.372 KHz (default) sts OCR4C, r1 // 10-bit write TC4H:OCR4C = 0x0FF // duty cycle = OCR4D/OCR4C = 100% sts OCR4D, r1 // 10-bit write TC4H:OCR4D = 0x0FF (Motor A) sts OCR4B, r1 // 10-bit write TC4H:OCR4B = 0x0FF (Motor B) sts TCCR4B, r0 // all configuration bits to default with prescalar = 0, OFF ldi r16, 0x09 sts TCCR4A, r16 // clear to manually control Motors A PWMD ldi r16, 0x2 sts TCCR4A, r16 // clear to manually control Motors B PWMB sts TCCR4D, r0 // mode = fast PWM (default) end of initialization Walk: push r16 ldi r16, 0x01 // configure inversion mode, reset, dead time to default = 0 sts TCCR4B, r16 // with prescaler = 1. Motors A and B Timer/Counter 4 ON pop r16 ret Stop: push r16 clr r16 // configure in fast PWM mode with prescaler = 0 sts TCCR4B, r16 // Motors A and B Timer/Counter 4 OFF pop r16 ret
Review Questions
- TBD
Answers
Using your mouse, highlight below in order to reveal the answers.
- TBD
Appendix A: Timer/Counter 4 Register Summary
Timer/Counter4 is a monster with five (5) control registers for configuring the timer/counter.
- TCCR4A – Timer/Counter4 Control Register A
- TCCR4B – Timer/Counter4 Control Register B
- TCCR4C – Timer/Counter4 Control Register C
- TCCR4D – Timer/Counter4 Control Register D
- TCCR4E – Timer/Counter4 Control Register E
Two registers used to make the 10-bit timer/counter.
- TC4H – Timer/Counter4 High Byte
- TCNT4 – Timer/Counter4
Four output compare registers
- OCR4A – Timer/Counter4 Output Compare Register A
- OCR4B – Timer/Counter4 Output Compare Register B
- OCR4C – Timer/Counter4 Output Compare Register C
- OCR4D – Timer/Counter4 Output Compare Register D
Two register to support polling and interrupts
- TIMSK4 – Timer/Counter4 Interrupt Mask Register
- TIFR4 – Timer/Counter4 Interrupt Flag Register
And one register unique to timer/counter4
- DT4 – Timer/Counter4 Dead Time Value
Unused Registers
The following registers are not used in our application and are kept at their default values (0x00).
- TCCR4E – Timer/Counter4 Control Register E
- OCR4A – Timer/Counter4 Output Compare Register A
- TIMSK4 – Timer/Counter4 Interrupt Mask Register
- TIFR4 – Timer/Counter4 Interrupt Flag Register
- DT4 – Timer/Counter4 Dead Time Value
Timer/Counter4 Interrupt Mask and Flag Registers (14.10.17, 14.10.19)
The OCF4B and OCF4D flag bits in the TIFR4 register will be set on compare match. As currently configured and defined in section “Timer/Counter4 Control Registers” the OCF4B and OC4D pins are cleared to 0 on compare match.
C++ Arrays, Data Structures, and working with Program Memory
Table of Contents
Reference(s):
Introduction
In our last C++ lecture we looked at simple datatypes like uint8_t. In this lecture we will be looking at “structured” data types. A structured data type is a collection of related data organized in some logical fashion and assigned a single name (identifier). Data structures may be accessed as a collection of data or by the individual data items that comprise them. Our first structured data type is the array.
C++ simple data types:
- integer (uint8_t, int16_t)
- float (float, double)
- enum
C++ structured data types:
- array
- struct
- union
- class
Arrays
- Arrays are groups of variables of the same data type.
- Variables within the array are organized sequentially and accessed using a simple numerical index. Arrays therefore occupy contiguous locations in memory (SRAM or Flash).
- The first entry has index value 0 and therefore the last entry has an index of “size – 1”, where size is the number of variables within the array.
Declaring Arrays
The simplest array declaration only has a single dimension.
dataType arrayName [ arraySize ];
The arraySize is an integer or constant variable (const int ARRAY_SIZE = 100) greater than zero. The dataType can be any C++ data type.
For example, consider rooms within a maze. Imagine declaring all the variables in the maze as simple data types and then consider how you would access each variable within the maze.
uint8_t room0, room1, room2 ... room99; // a maze with 100 rooms
Clearly, an array provides a much simpler and practical solution. The following C++ statement declares a collection of 100 unsigned 8-bit integers, named maze.
uint8_t maze[100]; // a maze with 100 rooms
Accessing a room within the maze is now as simple as giving its index within brackets []. The use of brackets [] in both the declaration and accessing elements of an array can potentially lead to confusion. In the declaration statement the brackets enclose the size of the array, while in the second case, the brackets enclose the index (base 0).
uint8_t room2 = maze[2];
Initializing Arrays
In C++ we initialize an array using braces {}, also known as curly brackets.
In this example, I want to track a robot’s progress in a maze containing obstacles. For fun the robot looks like a bear and the obstacles are defined as bee stings. As the robot moves from room-to-room the program needs to keep track of 6 variables.
To illustrative purposes, I am going to locate all this data in a single array in SRAM named myRobot containing 6 unsigned 8-bit integers corresponding to these data.
// dir turn row col room bees uint8_t myRobot[6] = {0x03, 0x00, 0x14, 0x00, 0x00, 0x00};
C++ provides a lot of flexibility in the declaration of an array. However, the array size once defined can not be changed (at least not within the context of an introductory lecture on C++ arrays), nor is this something you want to do in an embedded system. Lets look at two equivalent declaration statements for myRobot.
uint8_t myRobot[] = {0x03, 0x00, 0x14, 0x00, 0x00, 0x00}; // array holds these 6 bytes uint8_t myRobot[6] = {0x03, 0x00, 0x14}; // extra bytes assigned 0x00
As long as the number of values between braces { } is not larger than the number of elements that you declare for the array between the brackets [ ], no error will be generated.
Accessing Array Elements
As mentioned earlier, once an array is initialized, accessing a datum within the array is implemented by placing the index of the datum within square brackets after the name of the array.
uint8_t row = myRobot[2];
The above statement will take 3rd element from the array and assign the value to the row variable. Below is another example that declares, assigns random values, and accesses elements in an array.
void setup () { Serial.begin(9600); // generate random seed based on milliseconds since program started randomSeed( (uint8_t) millis()); // cast 32-bit unsigned long to 8-bits uint8_t n[10]; // n is an array of 10 8-bit unsigned integers // initialize each element of array n to a random number for (int i = 0; i < 10; i++) { n[i] = random(255); // random number between 0 and 255 } Serial.print("Element "); Serial.println("Value"); // output each array element's value for ( int i = 0; i < 10; i++) { Serial.print(" "); Serial.print(i); Serial.print(" "); Serial.println(n[i]); } }
Here is a sample output.
Index | Value |
0 | 36 |
1 | 94 |
2 | 54 |
3 | 45 |
4 | 94 |
5 | 170 |
6 | 103 |
7 | 162 |
8 | 3 |
9 | 8 |
Example – Running Average
In this example, I will show you how to implement a running average (digital low-pass filter) in your program using an array. It is very useful for inputs that have a high variance between samples (high frequency component), yet only vary slightly over time (low frequency component). An example would be the analog output of an IR sensor as read by the 10-bit ADC peripheral subsystem of an ATmega microcontroller.
Stability and accuracy of the final value will depend on the number of samples used to calculate the running average. If the data changes slowly relative to the sample frequency of the microcontroller, then a larger sample size can be employed.
The example program below reads from an analog pin and computes the running average for a sample size of 8.
Notes:
1. Alternative array initialization includes
window[g_width]; window[g_width] = {};
2. Alternative implementations of limit check on circular buffer include the modulus and “and” operator. The first alternative is not recommended without an fpu. The second alternative requires a buffer size that is a multiple of 2.
n % 8; n &= 0x000F;
3. You could also use a shift operator in place of division (multiple of 2) Unfortunately, our processor does not have a barrel shifter. Alternative float version: return sum/float(g_width);
C++ Passing Arrays to Functions
C++ does not allow you to pass an entire array as an argument to a function. However, you can pass a pointer to an array by specifying the array’s name without an index.
If you want to pass a single-dimension array as an argument in a function, you will have to declare a pointer to the array as a formal parameter in the function declaration. Here is a quick review of function declarations so that you understand what is being done here.
C++ Function Declarations – Review
When you need to create a custom function that will be used in your program, it must be declared and/or defined. The format for a declaration is shown below.
[Data type of output parameter] [Function Name] ([Data Types of input parameters]) {}
The first part of the function declaration is the data type of the output parameter. This is the type of value that will be returned from the function. This can be an int, float, pointer, or etc. If there is no value to be returned, you will use the void data type. You only need to indicate the data type, as there is no need to name the output.
The second part of the function declaration is the name of the function. This is what is used to call the function. A general convention for naming functions is to have the first word in lowercase and capitalize the first letter of the second word.
The third part of the function declaration are the data types of the input parameters. Any variables or values that the function will need to use must be defined here. This is related to the concept of the scope of a function because copies of those variables or values are created when the input parameters are defined. They are not readily available to the custom functions unless the variables or values have a global scope. It is generally not recommended to use global variables. If there are no inputs, you will have nothing inside of the parentheses.
The final part of a function declaration are the brackets. This is where the function is defined and the code to be performed are placed here. When programming within the Arduino IDE, you will not need to worry about where a function is declared or defined as long as it is located within the main “.ino” file. If a function is called near the beginning of the program and it is defined much later in the code, the compiler will not have an issue with finding the function and will not give a scope error. For this reason, you only need to declare and define a function a single time rather than provide a declaration as part of the header (.h) file, as is the case within traditional C++ IDEs, like Eclipse. Here is an example of a function declaration.
void myFunction(int input1, float input2, char input3) {}
This function is named myFunction and will return no output. It has three input values that are of three different types. The inputs are named input1, input2, and input3 and are only available to be used within the function. There are used more as place holders for the variable or value that you intend to use in the function.
If you are trying to call this example function, you will need to use the following code with the assumption that x is defined as an int, y is defined as a float, and z is defined as a char.
myFunction(x, y, z);
C++ Arguments versus Parameters – Review
Now that we have covered function declarations, it is important to note the distinction between arguments and parameters. A parameter is defined in the function declaration and indicates the type of data that is used. The parameter name is usually a placeholder to represent the kind of value or variable that the function works with. In the function declaration of myFunction the parameters are input1, input2, and input3.
An argument is the actual value or variable being used when a function is called. Arguments are passed to a function and are modified by the function code. From the example above, x, y, and z are the three arguments being sent to myFunction.
Defining A Pointer Parameter
After all of that review, we can now introduce how to define a pointer parameter for a function declaration. There are three ways to do this and all three declaration methods produce similar results because each tells the compiler that an integer pointer is going to be received.
Traditional Way
This form is the general form that has been used the longest and is distinct because of the use of the * to indicate it is a pointer. This is also the most outdated method and should not be used.
void myFunction(int * param) { // function definition }
Modern Way
This is the modern way to define a pointer parameter because it is clear that an array is being used and the intended size of that array is known.
void myFunction(int param[10]) { // function definition }
Modern Way without Bounds Checking
The final method is to define the pointer parameter as an unsized array that can work with any size array argument that is passed to it. As shown in the next example, this way can allow the definition of more general purpose functions. On the minus side, this way can not do bounds checking on your array (i.e., garbage in garbage out).
void myFunction(int param[]) { // function definition }
Now, consider the getAverage function, which takes an array as an argument along with its is size and based on this information returns the average of the numbers contained in the array.
int getAverage(int arr[], int size) { int sum = 0; for (int i = 0; i < size; ++i) { sum += arr[i]; } float avg = float(sum) / size; return (int) avg; // round up }
Now, lets call the above function.
void setup () { // an int array with 5 elements. int balance[5] = {1000, 2, 3, 17, 50}; // pass pointer to the array as an argument. int avg = getAverage(balance, 5); // balance argument is a pointer // output the returned value Serial.print(“Average value is: ”); Serial.print(avg); }
When the above code is compiled and run, it produces the following result
Average value is: 214
As you can see, the length of the array doesn’t matter as far as the function is concerned because C++ performs no bounds checking for the formal parameters.
Returning an Array from a Function
Just as you can not pass the contents of an array as an argument in a function call, you can not ask a function to return an array, instead you must return a pointer to an array instead. You can do this by defining the data type and using an * to indicate it is a pointer as shown below.
int * myFunction() {}
If the output array is created within the function, qualify the array data type as static, so the pointer being returned is not the address of a local variable that could disappear once the program is out of the scope of the function. What this is referring to is the fact that the program will delete all local variables after it returns from the function (technically the stack frame is destroyed) and the pointer is no longer pointing to the array. By using the static qualifier, it will preserve the data at the address defined by the pointer.
The example shown below will return a pointer to an array of 10 random numbers.
int * getRandom() { static int r[10]; randomSeed( (uint8_t) millis()); for (int i = 0; i < 10; ++i) { r[i] = random(255); // random number between 0 and 255 Serial.println(r[i]); } return r; }
You can also define a pointer as a variable and initialize its value to be the address of an array. The pointer will need to be the same data type as the array. The example shown below works with the getRandom() function.
int *p; p = getRandom();
By modifying the pointer, you can access the different elements of the array instead of using the index value. The code below will modify the 2nd element of the array of random numbers to be 20 and the 7th element to be 100.
*(p + 1) = 20; *(p + 6) = 100;
Structured Data Types in Program Memory (PROGMEM)
Reference:
- Arduino PROGMEM https://playground.arduino.cc/Main/PROGMEM
- Data in Program Space http://www.nongnu.org/avr-libc/user-manual/pgmspace.html
The C++ compiler was designed for computers based on the Princeton memory model. In a Princeton memory model both the program and the data share a common memory, in this case SRAM. Therefore, by default in C++ all arrays are created and stored within SRAM. Many embedded systems, like the AVR family of microcontrollers are based on the Harvard memory model. In a Harvard memory model, the program and data memory reside in different physical memories; specifically, Flash Program and SRAM respectively. To make matters worse, microcontrollers contain significantly more Flash than SRAM memory. For example, the ATmega32U4 contains 32 Kbytes of Flash memory versus only 2.5 Kbytes of SRAM. Therefore, if data in an array is constant and it takes up a lot of space, you should save it all in Flash program memory – but how when the C++ compiler want to put it all in SRAM?
In order to solve this conundrum within the Arduino IDE (GCC C++ compiler), we will be using the avr/pgmspace.h library that is available for the AVR architecture only. It provides definitions for the different data types that can be saved into Flash program memory and all of the functions needed to interact (read, find, compare, etc) with the data saved there. To use the library, you need to add the following line of code with other include statements at the top of the program.
Once it has been included, you can start defining the data to be saved into Flash program memory using the PROGMEM variable modifier. It can be added before the data type of the variable or after the variable name and it indicates that this variable (arrays included) is to be saved in Flash program memory. Below are some examples of how to use it.
const PROGMEM dataType variableName[] = {}; // I like this form const dataType variableName[] PROGMEM = {}; // this form also works const dataType PROGMEM variableName[] = {}; // but maybe not this one
- program memory dataType – any program memory variable type
- variableName – the name for your array of data
Rules for initialization of the arrays within Flash program memory are the same as conventional arrays.
const PROGMEM uint8_t myData[11][10] = { {0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09}, {0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10,0x11,0x12,0x13}, {0x14,0x15,0x16,0x17,0x18,0x19,0x1A,0x1B,0x1C,0x1D}, {0x1E,0x1F,0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27}, {0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0x30,0x31}, {0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x3B}, {0x3C,0x3D,0x3E,0x3F,0x40,0x41,0x42,0x43,0x44,0x45}, {0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F}, {0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57,0x58,0x59}, {0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,0x60,0x61,0x62,0x63}, {0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x6B,0x6C,0x6D} };
One trick, I have used in past is to create the array in Microsoft Excel and to export as CSV (Comma delimited) open in Notepad and then copy-paste into the program.
Now that your data resides in Flash program memory, your code to access (read) the data will no longer work The code that gets generated will retrieve the data that is located at the address of the myData array, plus offsets indexed by the i and j variables. However, the final address that is calculated points to the SRAM Data Space Not Flash Program Space where the data is actually located. So your program will be retrieving garbage. Again, the problem is that C++ compiler does not intrinsically know that the data resides in the Flash program memory.
Consequently, you will need to use the pgm_read_byte() or pgm_read_byte_near()functions. From my reading, for AVR microcontroller 16-bit address applications, pgm_read_byte() resolves to pgm_read_byte_near. One way to access the data bytes in the myData 2-dimensional array is by translating the SRAM base + index address into a Flash program memory address using the referencing operator ” .
uint8_t X = pgm_read_byte(&(myData[i][j])); // to learn more about this approach click here.
Here is another way.
uint8_t X = pgm_read_byte(myData + i*10 + j); // to learn more about this approach click here.
In the past I have only worked with one dimensional arrays. Therefore both approaches above are untested.
There are different functions for each of the data types (word, dword, float, and ptr). Click here for more information about these functions and the pgmspace library.
Here is a nice Arduino IDE script showing how to use PROGMEM and the pgmspace library to print out a 1-dimensional array of 16-bit unsigned word values and a text string.
// array of unsigned 16-bit integers const PROGMEM uint16_t charSet[] = { 65000, 32796, 16843, 10, 11234}; // array of 8-bit characters const PROGMEM char signMessage[] = {"Hello World"}; char myChar; void setup() { Serial.begin(9600); while(!Serial); // make sure buffer is empty // put your setup code here, to run once: // read back an array of 2-byte integers for (int k = 0; k < 5; k++){ uint8_t displayInt = pgm_read_word_near(charSet + k); Serial.println(displayInt); } Serial.println(); // read back an array of char int len = strlen_P(signMessage); for (k = 0; k < len; k++){ myChar = pgm_read_byte_near(signMessage + k); Serial.print(myChar); } Serial.println(); } void loop() { // put your main code here, to run repeatedly: }
Caveats
The macros and functions used to retrieve data from the Program Space generate some extra code in order to actually load the data from the Program Space. This incurs extra overhead in terms of code space (extra machine code instructions) and therefore execution time. Usually, both the space and time overhead is minimal compared to the space savings of putting data in Flash program memory. But you should be aware of this so you can minimize the number of calls within a single function that gets the same piece of data from Flash. It is always instructive and fun to look at the resulting disassembly from the compiler.
Data Structures
Data Structures in C++ provide a greater level of organization for complex systems. Before going into greater details arrays will be reviewed as they give a good insight on how structures work. for example: type array[size]; C++ arrays are a fixed length of a singular data type. This allows us to collect groups of numbers for data for analysis. The issue comes when collections of data are not conducive to arrays or if there is a mixture of data types. Starting from basic C++ foundation, the immediate idea is to make sets of arrays and organize the groups of information by index. That is the core premise of structures! Structures also lays a good foundation as to how Object Oriented Programming (OOP) is organized.
I will take my example from the myRobot array introduced at the beginning of this page. To review, I want to track a robot’s progress in a maze containing obstacles. For fun the robot looks like a bear and the obstacles are defined as bee stings. As the robot moves from room-to-room the program needs to keep track of 6 variables.
As an alternative to working with 6 variables at a time, I created a single array named myRobot containing 6 unsigned 8-bit integers corresponding to these data.
// dir turn row col room bees
uint8_t myRobot[6] = {0x03, 0x00, 0x14, 0x00, 0x00, 0x00};
While consolidating all this data into a single array, accessing the array does not add any clarity to my program. For example, if I wanted to find out the row the robot was in, I would add the code segment myRobot[2] – not very helpful. Data structures allow us to have the best of both worlds
I will now define a struct named MyRobot_t and group together all the variables we want to keep track of. From our discussion above or group will consist of variables dir, turn, row, col, room, and bees. The generation of this data structure looks like this:
struct MyRobot_t { uint8_t dir; uint8_t turn; uint8_t row; uint8_t col; uint8_t room; uint8_t bees; } MyRobot_t Robot
This definition needs to be placed within a header file or else the Arduino IDE will return several errors when we try to use the structure. We will learn more about header files in the future. For example, in Lab 4 the code above is placed in a header file named maze.h. With this structure, we have defined all of our variables as uint8_t but they can be any type. In the main loop, we can create a static structure to hold all of our relevant data.
Wait, we discussed that structs can hold multiple data types but we are holding only one type here. Why not use an array? While you can definitely do the same thing with an array, structs make the code more readable. Now instead of Robot[1] = newTurn;
The instruction would be Robot.turn = newTurn;
Since we need all of our updating functions to have access to the information we will define a static struct inside loop. The struct is now functionally a data type so many of the same rules apply. (const/static/ scope/ etc.) Our Instantiation of the Struct will look like.
void loop() { static MyRobot_t Robot{0x03,0x00,0x14,0x00,0x00,0x00}; // more code }
This creates a structure called Robot that initializes the values of each variable to the values listed (similar to an array). The list of default values fills the array in the order it was declared in our header file. So for our case dir = 0x03, turn = 0x00 and etc. If you want to be more rigid in your default settings you may define this in the structure prototype as shown below.
In our header…
struct MyRobot_t {
uint8_t dir = 0x03;
uint8_t turn = 0x00;
.
.
.
etc
}
Object Oriented Programming Example
Class.h File
class sensor { private: /* these properties can't be modified outside the class */ // defined by the constructor uint8_t pin; // IR analog pin (A0, A1, A2, A3) float Vref = 3.3; // analog reference voltage uint8_t buffer_depth; // number of 10-bit ADC samples averaged // internal properties // over the data stream. maximum size = 64 (2^6) uint8_t pointer = 0; // index pointer into the buffer // points to oldest value uint16_t circular_buffer[64] = {0}; // window into the data stream. uint16_t sum = 0; // sum of the readings in the buffer public: /* these methods can be called outside the class and can use the private properties */ // constructor example sensor(A0,8,3.3) sensor(uint8_t ic_pin,uint8_t circular_buffer_depth,float voltage_reference){ pin = ic_pin; buffer_depth = circular_buffer_depth; Vref = voltage_reference; // fill the buffer for (int i=0; i < buffer_depth;i++){ readSensor(); } } void readSensor(){ int16_t A_1 = analogRead(pin); // 1. read analog value uint16_t A_0 = circular_buffer[pointer]; // 2. get oldest reading sum += A_1 - A_0; // 3. subtract out oldest reading and // replace with current reading // update circular_buffer circular_buffer[pointer] = A_1; pointer++; if(pointer >= buffer_depth) pointer = 0; } uint16_t getAvg(){ return sum/buffer_depth; } float getVpin(){ return float(getAvg()) * Vref/1024.0; } };
Arduino Main Page
#include "3DoTconfig.h" #include "Class.h" sensor IR_R(A0,8,3.3); // instantiate right IR sensor sensor IR_L(A2,8,3.3); // instantiate left IR sensor void setup() { } void loop() { IR_R.readSensor(); uint16_t average = IR_R.getAvg(); float voltage = IR_R.getVpin(); Serial.print("Average: "); Serial.println(average); Serial.print("Voltage: "); Serial.println(voltage); delay(500); }
Review Questions
- The following code will create an array of how many elements? int testarray[14];
- Write the code that will assign the value of the 3rd element of the array called
balance to a variable called change - Can an array be defined without an array size?
- The action of sending variable values to be used in a function is called what?
- The void data type is used for what?
- Write one of the ways to define a pointer as an input parameter for the function
testFunction() - Write the function definition for a function called returnPointer that has no input
parameters and returns a pointer to an integer - Where are arrays normally saved to?
- What function from the pgmspace library should be used to read a value from an
array? - What type of buffer is required to implement a running average?
Answers
Using your mouse, highlight below in order to reveal the answers.
- 14 elements
- change = balance[2];
- No, all arrays must have a defined size.
- Passing arguments (may need to remove “values” from the question to be less
confusing) - Void is used for defining that a function does not have an output returned
- Void testFunction(int *pointer) {} , void testFunction(int pointer[]) {}, void
testFunction(int pointer[10]) {} - int *pointer returnPointer() {}
- SRAM
- pgm_read_byte_near()
- circular
Appendix A Statistics Package
int readADC(int sensorPin){ int n = 200; // number of ADC samples int x_i; // ADC input for sample i float A_1; // current i running average float A_0; // previous i-1 running average // rapid calculation method http://en.wikipedia.org/wiki/Standard_deviation A_0 = 0; for (int i=1; i <= n; i++){ x_i = analogRead(sensorPin); A_1 = A_0 + (x_i - A_0)/i ; A_0 = A_1; } // Serial.print(", mean = "); // Serial.println(A_1); return (int(A_1)); }
PID Controllers
Table of Contents
Reference(s):
- AVR221: Discrete PID Controller on tinyAVR and megaAVR devices
- MIT Lab 4: Motor Control introduces the control of DC motors using the Arduino and Adafruit motor shield. A PID controller is demonstrated using the Mathworks SISO Design Tools GUI with accompanying Mathworks PID tutorial “Designing PID Controllers.”
- RepRap Extruder Nozzle Temperature Controller. RepRap provides a feed forward example (PIDSample3) and can be found in the PID_Beta6 project folder.
PID Theory
In Figure 1 a schematic of a system with a PID controller is shown. The PID controller compares the measured process value y with a reference setpoint value, y0. The difference or error, e, is then processed to calculate a new process input, u. This input will try to adjust the measured process value back to the desired setpoint.
The alternative to a closed-loop control scheme such as the PID controller is an open-loop controller. Open-loop control (no feedback) is in many cases not satisfactory and is often impossible due to the system properties. By adding feedback from the system output, performance can be improved.
Unlike a simple proportional control algorithm, the PID controller is capable of manipulating the process inputs based on the history and rate of change of the signal. This gives a more accurate and stable control method.
The basic idea is that the controller reads the system state by a sensor. Then it subtracts the measurement from the desired reference to generate the error value. The error will be managed in three ways, too…
- handle the present, through the proportional term,
- recover from the past, using the integral term,
- anticipate the future, through the derivative term.
Click here to continue the reference article.
From Theory to Programming
The layout in Figure 2 reflects the “classic” PID architecture, naming conventions, and software implementation, versus the primary reference article. Figure 2 shows the PID design by, where Kp, Ki, and Kd denote the time constants of the proportional, integral, and derivative terms respectively. Within the literature, the variable, block, and even layouts may change, while the fundamentals stay the same.
In this section, I am going to step through the blocks defined in Figure 2 “PID controller schematic” and look at how they have been translated into software.
- Error value
- Proportional term
- Integral term
- Differential term
- Summing junction
The software examples are from these PID controllers.
- My Little PID Controller
- Bare Bones (PID) Coffee Controller
- AeroQuad
Error Value
The error value e(t) is defined as the difference between the desired setpoint r(t) and a measured process variable y(t). Here is the C++ statement used by the Bare Bones Coffee control software to implement this term.
// determine how badly we are doing // error = setpoint - process value double error = sp - pv;
Proportional Term
The proportional term is only a function of the error. Here is the C++ statement used by the Bare Bones Coffee control software to implement this term.
// the pTerm is the view from now, the pgain judges // how much we care about error we are at this instant. double pTerm = kp * error;
The proportional term (P) gives a system control input proportional to the error. Using only P control gives a stationary error in all cases except when the system control input is zero and the system process value y(t) equals the desired value.
In the figure below the stationary error e(t) in the system process value y(t) appears after a change in the desired value r(t). The response shown is that of a second-order transient system. Using a too large P term gives an unstable system.
Integral Term
You may remember the definition of integration, as shown in Figure 4, from your calculus class. If you have forgotten, the image below was originally presented in this review article. To program the integral term we work backward from the definition by approximating the integral term by taking the area under the error(t) curve.
Here is the C++ statement used by My Little PID control software to implement this term.
static double iTerm = 0; iTerm += ki * error; // ki unit time (ki = Ki*dT)
Here is the C++ statement used by the AeroQuad PID control software to implement this term. Where the arrow operator -> is used to access the integratedError member of the PIDparameters data structure by reference.
PIDparameters->integratedError += error * G_Dt;
Where global variable for delta time [1] is defined as.
float G_Dt = 0.02;
[1] Of all the PID code examples reviewed for this article, this was the only one that included delta time (Dt). All other code examples combine delta time with the integral term (Ki). This latter approach is computationally more efficient at the cost of clarity (see Computational Simplification).
The figure below shows the step responses to an I and PI controller. As seen the PI controller response has no stationary error and the I controller response is very slow.
Differential Term
Returning to your first course in calculus you hopefully also remember the definition of derivative as explained here. To program the differential term we again work backward from the definition by approximating the differential term by finding the slope of the error(t) curve at time t.
Here is a C++ statement for the My Simple PID control software to implement this term.
double dTerm = kd * (pv - last_value); // kd unit time (kd = Kd/dT) last_value = pv;
Here is a C++ statement, written in the form of the AeroQuad PID control software to implement this term.
PIDparameters->differentialError = (currentPosition - PIDparameters->lastPosition) / G_Dt;
The figure below shows D and PD controller responses. The response of the PD controller gives a faster rising system process value.
The differential term essentially behaves as a high pass filter on the system error signal and may therefore increase the effect of noise on the system leading to system instability. To see if a differential term is the best choice for your application, I would recommend reading “The PID’s Derivative Term Can Improve Control Loop Performance, But Often at a Cost.”
Summing Junction
The summing junction u(t) adds the control inputs to be applied to the plant from the Proportional, Integral, and Differential blocks. Depending on the software implementation the gain factors Kp, Ki, and Kd may be added here, or within each computational block. The summing junction is often included in the return statement.
The My Little PID controller takes the former approach as shown here.
return pTerm + iTerm + dTerm; // process input
The Bare Bones Coffee controller takes the former approach as shown here.
// the magic feedback bit return pTerm + iTerm - dTerm;
The AeroQuad PID controller takes a hybrid approach with the gain factors P and I included at the summing junction, versus the differential block, where the term already includes gain factor D.
return (PIDparameters->P * error) + (PIDparameters->I * (PIDparameters->integratedError)) + dTerm;
The figure below compares the P, PI, and PID controllers. PI improves the P by removing the stationary error, and the PID
improves the PI by the faster response and no overshoot.
Computational Simplification
In moving from theory to program implementation, I intentionally used the AeroQuad as the example for computation of the integral term because it followed directly from the definition of integration and specifically included delta time.
PIDparameters->integratedError += error * G_Dt;
I then took some liberties by creating a differential term that also included time.
PIDparameters->differentialError = (currentPosition - PIDparameters->lastPosition) / G_Dt;
Here is the actual AeroQuad computation of the differential term.
dTerm = PIDparameters->D * (currentPosition - PIDparameters->lastPosition);
The inclusion of the differential gain term D, as mentioned in the discussion of the summing junction, is not unusual; but what happened to time? The better question would surprisingly be, why does the AeroQuad include time in the computation of the integral term in the first place? In almost every software PID controller, time is assumed to be a constant and therefore incorporated into the gain terms to minimize computational time. I was lucky to find one of the few exceptions. To understand why this is true, let’s take a look at a hypothetical PID controller (based on the Bare Bones Coffee controller) where the PID terms include both the gain terms and delta time.
// iState keeps changing over time; it's // overall "performance" over time, or accumulated error iTerm += igain * error * dT;
// the dTerm, the difference between the temperature now // and our last reading, indicated the "speed," // how quickly the temp is changing. (aka. Differential) dTerm = dgain * (curTemp - lastTemp) / dT;
By application of the associative (iTerm and dTerm) and distributive (dTerm) laws, these two C++ statements could be rewritten as follows.
iTerm += (igain * dT) * error; dTerm = (dgain / dT) * (curTemp - lastTemp);
The answer to our question is now more obvious. To compute the iTerm two multiplication operations are required. In the same way, the dTerm requires division, multiplication, and subtraction. By redefining the gain terms to include the time which we again assume is a constant, the iTerm only requires a single multiplication and the dTerm a single multiplication and subtraction. The Arduino PID library v1.2.1 and My Simple PID controller provides a nice example of this integration of gain terms and time in their tuning function. Here is a simplified version of the SetTunings function.
/* SetTunings(...)************************************************************* * This function allows the controller's dynamic performance to be adjusted. * it's called automatically from the constructor, but tunings can also * be adjusted on the fly during normal operation * source: Arduino PID Library - Version 1 * by Brett Beauregard <br3ttb@gmail.com> brettbeauregard.com ******************************************************************************/ void setTunings(double Kp, double Ki, double Kd) { if (Kp<0 || Ki<0 || Kd<0) return; double dispKp = Kp; double dispKi = Ki; double dispKd = Kd; double SampleTimeInSec = ((double)dT)/1000; kp = Kp; ki = Ki * SampleTimeInSec; kd = Kd / SampleTimeInSec; }
Discrete PID Controller – Sample Period
From the last section, we now know that a discrete PID controller will read the error, calculate and output the control input at a constant time interval (sample period dT). So how do I choose a sample time? The sample time should be less than the shortest time constant (36% of normalized output) in the system. This represents the slowest acceptable sample time; hopefully, your system can sample/control the system at a much higher rate.
For PID controllers in which the measurement of the process value y(t) incorporates a gyro (including IMUs) setting the sample period to high will result in an increase in the integration error from the gyro (converting from angular velocity to an angle). Note: this is a different integration than the I in PID.
Read “Improving the Beginner’s PID – Sample Time” by Brett Beauregard, the author of the Arduino PID controller, to learn how time is handled by his PID controller.
Windup
Source: AVR221: Discrete PID controller
When the process input, u(t), reaches a high enough value, it is limited in some way. Either by the numeric range internally in the PID controller, the output range of the controller or constraints in amplifiers, or the process itself. This will happen if there is a large enough difference in the measured process value and the reference setpoint value, typically because the process has a larger disturbance/load than the system is capable of handling, for example, at startup and/or reset.
If the controller uses an integral term, this situation can be problematic. The integral term will sum up as long as the situation lasts, and when the larger disturbance/load disappears, the PID controller will overcompensate the process input until the integral sum is back to normal. This problem can be avoided in several ways. In this implementation, the maximum integral sum is limited by not allowing it to become larger than MAX_I_TERM. The correct size of the MAX_I_TERM will depend on the system and sample time used.
Here is how My Simple PID controller mitigates windup.
static double iTerm = 0; iTerm += ki * error; // ki unit time (ki = Ki*dT) iTerm = constrain(windupGuardMin, iTerm, windupGuardMax);
A standard C++ alternative to Arduino’s unique constrain() function is provided here.
if(iTerm > outMax) iTerm= outMax; else if(iTerm < outMin) iTerm= outMin;
Here is how the Bare Bones Coffee controller mitigates windup.
// iState keeps changing over time; it's // overall "performance" over time, or accumulated error iState += error; // to prevent the iTerm getting huge despite lots of // error, we use a "windup guard" // (this happens when the machine is first turned on and // it cant help be cold despite its best efforts) // not necessary, but this makes windup guard values // relative to the current iGain windupGuard = WINDUP_GUARD_GAIN / igain; if (iState > windupGuard) iState = windupGuard; else if (iState < -windupGuard) iState = -windupGuard; iTerm = igain * iState;
Here is how the AeroQuad controller mitigates windup.
PIDparameters->integratedError += error * G_Dt; PIDparameters->integratedError = constrain(PIDparameters->integratedError, -windupGuard, windupGuard);
Where the windupguard is stored in EEPROM and defined here.
float windupGuard; // Read in from EEPROM
Proportional on Measurement
While the integration term is helpful in removing the constant offset error inherent in the proportional controller (you need some error to generate the proportional term in the first place), the I-term itself, even with windup, is a source of error when the setpoint is changed, for example on start-up. In these situations, the integration term will increase up to the windup guard value and must be removed over time. This can only be done by the system overshooting the setpoint (negative error) in order to subtract out the error. The bad news is that all the PID controllers used as case studies will exhibit this behavior. The good news is that the Arduino PID controller version 1.2.1 and later, includes Proportional on Measurement (PonM) tuning, which addresses this problem.
Brett Beauregard, the author of the Arduino PID library, has written an excellent series of articles on the new Arduino PID library. Start with the article entitled “Improving the Beginner’s PID – Introduction.” Clicking the Next>> button at the end of each article will take you in-depth on this PID controller. To learn more about WindUp, read the second article entitled “Improving the Beginner’s PID: Reset Windup.” To learn more about PonM, read the second article entitled “Introducing Proportional On Measurement.”
PID Control Examples
In this section, I am going to look at three control examples.
- Bare Bones (PID) Coffee Controller
- AeroQuad
- Arduino PID library with accompanying Tutorial “Improving the Beginner’s PID: Direction” by Brett Beauregard
The first three control examples are presented in order of the complexity of the PID controller implementation. The coffee controller is a single PID, which can be documented in a single Arduino ino file. The AeroQuad PID software is a modified version of the BBCC: Bare Bones (PID) Coffee Controller with the ability to control multiple control loops. The PID_Beta6 is the Beta version of the Arduino PID library, superseded by PID_v1.
BBCC | AeroQuad | PID_Beta6 | PID_v1 | |
Complexity Factor | low | medium | high | high |
Ability to Change Tunings on the Fly | ? | ? | yes | yes |
Inputs Normalized | no | no | yes | |
Input and Output Limits | no | no | yes | |
Multiple Control Loops | no | yes | yes | |
Reset-Windup Mitigation | somewhat | somewhat | yes | yes |
Proportional on Measurement | no | no | no | yes |
Proportional on Error | yes | yes | yes | yes |
Derivative Kick | no | no | no | yes |
Feed forward | no | no | yes | |
Integration Calculation includes dT | no | yes | no | |
Tuning | Processing | Labview | Processing |
Like the AeroQuad PID and the coffee controller do not normalize the input. Both include non industrial-standard reset-windup mitigation code. Unlike the AeroQuad PID and Arduino PID library (PID_Beta6), the coffee controller does not calculate the integral term as a function of delta time.
Tuning
Tuning the PID is where most of the “magic wand” action occurs. For some of the software control examples, the term “Configurator” is used for the development environment used for tuning the PID. It is not clear what IDE was used to develop the Configurator used by AeroQuad. The Coffee Controller, like the Arduino Graphical User Interface (GUI), uses Processing as its IDE for developing a simple configurator. The PIDLibrary also used Processing as illustrated here.
Simple Tuning Method
- Turn all gains to 0
- Begin turning up proportional gain until the system begins to oscillate.
- Reduce the proportional gain until the oscillations stop, and then back down by about 20%.
- Slowly increase the derivative term to improve response time and system stability.
- Increase the integral term until the system reaches the point of instability and then back it off slightly.
Ziegler-Nichols method
The Ziegler-Nichols method is outlined in the AVR221: Discrete PID controller article and Jordan’s PowerPoint presentation. The first step in this method is setting the I and D gains to zero, increasing the P gain until a sustained and stable oscillation (as close as possible) is obtained on the output. Then the critical gain Kc and the oscillation period Pc is recorded and the P, I and D values are calculated.
Fitting a simple first order plus dead time dynamic model to process test data.
This tuning method has been ported to the mbed platform. Here is a very nice step-by-step Tuning Example using mbed.
IMC Tuning Method
Since 1942, over one hundred tuning methods have been developed. One of these methods is the Internal Model Control (IMC) tuning method, sometimes called Lambda tuning. This Application Note describes how to tune control loops using IMC tuning rules.
Tuning the Arduino PID Controller
Read “Improving the Beginner’s PID: Tuning” by Brett Beauregard, the author of the Arduino PID controller, to learn how tuning is handled by his PID controller.
PID Control Software Examples
The next three sections, provide the code used for three different PID controllers. To help compare the programs I have color coded the parameters as defined here.
- Summing Junction – Sky Blue
- Proportional Term – Magenta
- Integral Term – Sea Green
- Differential Term – Royal Blue
My Little PID Control Software
/********************************************************************************************** * My Little PID Controller - Version 1 * * by Gary Hill <hellogaryhill@gmail.com> * * March 24, 2021 * * This is a module that implements a PID control loop * initialize it with 3 values: Kp,Ki,Kd * and then tune the feedback loop with the setP etc funcs * * This was written based PID controllers by * Tim Hirzel * Bare Bones Coffee Controller PID * http://www.arduino.cc/playground/Main/BarebonesPIDForEspresso#main * http://processing.org/ * Tim Wescott * http://www.embedded.com/2000/0010/0010feat3.htm * Brett Beauregard <br3ttb@gmail.com> brettbeauregard.com * Arduino PID Library * * All code released under * This Code is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License. **********************************************************************************************/ double kp = 100.0; double ki = 100.0; double kd = 100.0; const double dT = 100.0; // sample time in milliseconds const double windupGuardMin = 0; // set to output limit const double windupGuardMax = 255; // set to output limit void setup() { // Initialize serial and wait for port to open: Serial.begin(9600); while (!Serial) {} // wait for serial port to connect. Needed for native USB port only } void loop() { static double sp = 0; // set point static double updatePID = 0.0; if (updatePID >= millis()){ uint16_t process_value = analogRead(A4); // value 0 - 1023 double process_input = PID( (double) process_value, sp); analogWrite( LED_BUILTIN, (uint8_t) process_input); // value 0 - 255 } } /* Input parameter pv = process value, sp = set point * Function returns process input value */ double PID(double pv, double sp){ static double last_value = 0; static double i_err = 0; double error = sp - pv; // error value double pTerm = kp * error; // proportional term static double iTerm = 0; iTerm += ki * error; // Ki unit time (ki = Ki*dT) iTerm = constrain(windupGuardMin, iTerm, windupGuardMax); double dTerm = kd * (pv - last_value); // Kd unit time (kd = Kd/dT) last_value = pv; return pTerm + iTerm + dTerm; // process input } /* SetTunings(...)************************************************************* * This function allows the controller's dynamic performance to be adjusted. * it's called automatically from the constructor, but tunings can also * be adjusted on the fly during normal operation * source: Arduino PID Library - Version 1 * by Brett Beauregard <br3ttb@gmail.com> brettbeauregard.com ******************************************************************************/ void setTunings(double Kp, double Ki, double Kd) { if (Kp<0 || Ki<0 || Kd<0) return; double dispKp = Kp; double dispKi = Ki; double dispKd = Kd; double SampleTimeInSec = ((double)dT)/1000; kp = Kp; ki = Ki * SampleTimeInSec; kd = Kd / SampleTimeInSec; } /* Tips and Tricks * * You can also update on timer overflow bit associated * with the motors or Arduino built 8-bit timer T0. * By Alexander Littleton, DeRobot 2021 * * For standard C++ alternative to Arduino unique constrain() * if(iTerm > outMax) iTerm= outMax; * else if(iTerm < outMin) iTerm= outMin; * */
AeroQuad PID Control Software
Reference: AeroQuad Downloads
The following AeroQuad header and pde files are key to understanding the AeroQuad PID software.
AeroQuad.h
This header file defines AeroQuad mnemonics
// Basic axis definitions #define ROLL 0 #define PITCH 1 #define YAW 2 #define THROTTLE 3 #define MODE 4 #define AUX 5 #define XAXIS 0 #define YAXIS 1 #define ZAXIS 2 #define LASTAXIS 3 #define LEVELROLL 3 #define LEVELPITCH 4 #define LASTLEVELAXIS 5 #define HEADING 5 #define LEVELGYROROLL 6 #define LEVELGYROPITCH 7 float G_Dt = 0.02;
DataStorage.h
This header file is used to read and write default settings to the ATmega EEPROM.
// contains all default values when re-writing EEPROM void initializeEEPROM(void) { PID[ROLL].P = 1.2; PID[ROLL].I = 0.0; PID[ROLL].D = -7.0; PID[PITCH].P = 1.2; PID[PITCH].I = 0.0; PID[PITCH].D = -7.0; PID[YAW].P = 3.0; PID[YAW].I = 0.0; PID[YAW].D = 0.0; PID[LEVELROLL].P = 7.0; PID[LEVELROLL].I = 20.0; PID[LEVELROLL].D = 0.0; PID[LEVELPITCH].P = 7.0; PID[LEVELPITCH].I = 20.0; PID[LEVELPITCH].D = 0.0; PID[HEADING].P = 3.0; PID[HEADING].I = 0.0; PID[HEADING].D = 0.0; PID[LEVELGYROROLL].P = 1.2; PID[LEVELGYROROLL].I = 0.0; PID[LEVELGYROROLL].D = -14.0; PID[LEVELGYROPITCH].P = 1.2; PID[LEVELGYROPITCH].I = 0.0; PID[LEVELGYROPITCH].D = -14.0; windupGuard = 1000.0;
FlightControl.pde
This C++ program calls the PID updatePID function and zeroIntegralError subroutine. Here are a few example calls.
updatePID(receiver.getData(ROLL), gyro.getFlightData(ROLL) + 1500, &PID[ROLL])); updatePID(receiver.getData(PITCH), gyro.getFlightData(PITCH) + 1500, &PID[PITCH])); updatePID(receiver.getData(YAW) + headingHold, gyro.getFlightData(YAW) + 1500, &PID[YAW]
PID.h
The PID data structure and PID algorithm
struct PIDdata { float P, I, D; float lastPosition; float integratedError; } PID[8]; float windupGuard; // Read in from EEPROM // Modified from http://www.arduino.cc/playground/Main/BarebonesPIDForEspresso float updatePID(float targetPosition, float currentPosition, struct PIDdata *PIDparameters) { float error; float dTerm; error = targetPosition - currentPosition; PIDparameters->integratedError += error * G_Dt; PIDparameters->integratedError = constrain(PIDparameters->integratedError, -windupGuard, windupGuard); dTerm = PIDparameters->D * (currentPosition - PIDparameters->lastPosition); PIDparameters->lastPosition = currentPosition; return (PIDparameters->P * error) + (PIDparameters->I * (PIDparameters->integratedError)) + dTerm; } void zeroIntegralError() { for (axis = ROLL; axis < LASTLEVELAXIS; axis++) PID[axis].integratedError = 0; }
Bare Bones (PID) Coffee Controller
As commented on in the code, the AeroQuad PID software is a modified version of the BBCC: Bare Bones (PID) Coffee Controller The coffee controller is a single PID and so is a little simpler to understand.
Like the AeroQuad PID, the coffee controller does not normalize the input. Both include non industrial-standard reset-windup mitigation code. Unlike the AeroQuad PID, the coffee controller does not calculate the integral term as a function of delta time.
(PID_Beta6)
float updatePID(float targetTemp, float curTemp) { // these local variables can be factored out if memory is an issue, // but they make it more readable double result; float error; float windupGuard; // determine how badly we are doing // error = setpoint - process value error = targetTemp - curTemp; // the pTerm is the view from now, the pgain judges // how much we care about error we are at this instant. pTerm = pgain * error; // iState keeps changing over time; it's // overall "performance" over time, or accumulated error iState += error; // to prevent the iTerm getting huge despite lots of // error, we use a "windup guard" // (this happens when the machine is first turned on and // it cant help be cold despite its best efforts) // not necessary, but this makes windup guard values // relative to the current iGain windupGuard = WINDUP_GUARD_GAIN / igain; if (iState > windupGuard) iState = windupGuard; else if (iState < -windupGuard) iState = -windupGuard; iTerm = igain * iState; // the dTerm, the difference between the temperature now // and our last reading, indicated the "speed," // how quickly the temp is changing. (aka. Differential) dTerm = (dgain* (curTemp - lastTemp)); // now that we've use lastTemp, put the current temp in // our pocket until for the next round lastTemp = curTemp; // the magic feedback bit return pTerm + iTerm - dTerm; }
PIDLibrary – PID_Beta6.cpp
You will need AVR Studio to view this file with color coding the C++ code.
/* Compute() ****************************************************************** * This, as they say, is where the magic happens. this function should * be called every time "void loop()" executes. the function will decide * for itself whether a new pid Output needs to be computed * * Some notes for people familiar with the nuts and bolts of PID control: * - I used the Ideal form of the PID equation. mainly because I like IMC * tunings. lock in the I and D, and then just vary P to get more * aggressive or conservative * * - While this controller presented to the outside world as being a Reset * Time controller, when the user enters their tunings the I term is * converted to Reset Rate. I did this merely to avoid the div0 error * when the user wants to turn Integral action off. * * - Derivative on Measurement is being used instead of Derivative on Error. * The performance is identical, with one notable exception. DonE causes a * kick in the controller output whenever there's a setpoint change. * DonM does not. * * If none of the above made sense to you, and you would like it to, go to: * http://www.controlguru.com. Dr. Cooper was my controls professor, and * is gifted at concisely and clearly explaining PID control ****************************************************************************** */ void PID::Compute() { justCalced=false; if (!inAuto) return; //if we're in manual just leave; unsigned long now = millis(); //millis() wraps around to 0 at some point. depending on the version of the //Arduino Program you are using, it could be in 9 hours or 50 days. //this is not currently addressed by this algorithm. //...Perform PID Computations if it's time... if (now>=nextCompTime) { //pull in the input and setpoint, and scale them into percent span double scaledInput = (*myInput - inMin) / inSpan; if (scaledInput>1.0) scaledInput = 1.0; else if (scaledInput<0.0) scaledInput = 0.0; double scaledSP = (*mySetpoint - inMin) / inSpan; if (scaledSP>1.0) scaledSP = 1; else if (scaledSP<0.0) scaledSP = 0; //compute the error double err = scaledSP - scaledInput; // check and see if the output is pegged at a limit and only // integrate if it is not. (this is to prevent reset-windup) if (!(lastOutput >= 1 && err>0) && !(lastOutput <= 0 && err<0)) { accError = accError + err; } // compute the current slope of the input signal // we'll assume that dTime (the denominator) is 1 second. double dMeas = (scaledInput - lastInput); // if it isn't, the taud term will have been adjusted // in "SetTunings" to compensate //if we're using an external bias (i.e. the user used the //overloaded constructor,) then pull that in now if(UsingFeedForward) { bias = (*myBias - outMin) / outSpan; } // perform the PID calculation. double output = bias + kc * (err + taur * accError - taud * dMeas); //make sure the computed output is within output constraints if (output < 0.0) output = 0.0; else if (output > 1.0) output = 1.0; lastOutput = output; // remember this output for the windup // check next time lastInput = scaledInput; // remember the Input for the derivative // calculation next time //scale the output from percent span back out to a real world number *myOutput = ((output * outSpan) + outMin); nextCompTime += tSample; // determine the next time the computation // should be performed if(nextCompTime < now) nextCompTime = now + tSample; justCalced=true; //set the flag that will tell the outside world // that the output was just computed } }
PID Controller for Line Following Robots
By Jordan Smallwood | October 4th, 2017
Overview
The following PowerPoint presentation introduces Proportional Integral Derivative (PID) controllers and their application to a line following robot. The presentation concludes with an in-depth look with examples of integration into a line following robot in C++.
References
- https://en.wikipedia.org/wiki/PID_controller
- https://www.cds.caltech.edu/~murray/courses/cds101/fa04/caltech/am04_ch8-3nov04.pdf
Review Questions
- To Be Written
Answers
Using your mouse, highlight below in order to reveal the answers.
- To Be Written
ATmega Analog-to-Digital Conversion
Table of Contents
Reference(s):
- The AVR Microcontroller and Embedded Systems using Assembly and C by
Muhammad Ali Mazidi, Sarmad Naimi, and Sepehr Naimi, Chapter 13 ADC, DAC, and Sensor Interfacing - ATMEL 8-bit AVR Microcontroller with 4/8/16/32K Bytes In-System Programmable Flash, Chapter 23 “Analog-to-Digital Converter”
- AVR Freaks Newbie’s Guide to the AVR ADC by Ken Worster
- Successive Approximation ADC, Georgia State University, Department of Physics and Astronomy
- Successive approximation ADC, Wikipedia
ATmega MCU ADC Subsystem Features
- 10/8-bit Resolution
- 0.5 LSB Integral Non-linearity
- ± 2 LSB Absolute Accuracy
- Temperature sensor
- Single Conversion, Free Running, and 3DoT Mode 8
- Interrupt on ADC Conversion Complete
- Optional Left Adjustment for ADC Result Readout
- 2.56 V ADC Reference Voltage
- Up to 15 kSPS at Maximum Resolution with a conversion times of 13 μs (ATmega328P) and 65 – 260 μs (ATmega32U4)
- Six (ATmega328P), twelve (ATmega32U4), and five (3DoT) Multiplexed Single-Ended Input Channels
- One Differential amplifier providing gain of 1x – 10x – 40x – 200x (ATmega32U4 only)
- Sleep Mode Noise Canceler (ATmega32U4 only)
How it Works
What is a Successive Approximation ADC?
Illustrations from Tocci, Ronald J., Digital Systems, 5th Ed, Prentice-Hall, 1991.
A successive approximation ADC is a type of analog-to-digital converter that converts a continuous analog waveform into a discrete digital representation via a binary search through all possible quantization levels before finally converging upon a digital output for each conversion.
The successive approximation Analog to digital converter circuit typically consists of four chief subcircuits. Figure 1 is an example 4-bit ADC and will be used to illuminate how these four subcircuits work together to convert an analog value into a digital number.
- A sample & hold comparator circuit acquires the analog input voltage (Vs) and compares it to the output of an internal DAC with input reference voltage (Vref). To keep the illustration as simple as possible, this reference voltage is not shown and may be assumed to be equal to 15 v. In our example 4-bit DAC the analog input voltage (Vs) is set to 7.2 volts.
- The result of the comparison is sent to a successive approximation register (SAR). Identified as control logic and bits D3 to D0 in our simplified 4-bit DAC block diagram.
- The internal DAC supplies the comparator with an analog voltage equivalent of the digital code output of the SAR for comparison with Vs.
- The SAR subcircuit, a finite state machine, implements the algorithm defined in Figure
Using the Built-in ADC in the ATmega MCU
The ATmega datasheet provides everything you need to know to use the ADC subsystem of our AVR microcontroller. In the following sections I am going to focus on those topics which I consider to be most relevant when wanting to use the ADC:
- How to connect the pins related to the ADC (Voltage Reference).
- How to make an Analog to Digital conversion within the Arduino IDE.
- A Simple Analog to Digital Conversion (analogRead)
- The registers of the ADC (ADMUX, ADCSRA, and ADCH:ADCL). ADC registers ADCSRB and DIDR0 are left at default values and considered outside the scope of this introductory lesson.
- How to select an operating mode (Single Conversion and Free-Running)
- How to specify resolution/conversion speed (Sample Frequency).
- How to verify conversion complete (polling the ADSC bit).
I have tried to weave these topics into a single story centered around the ATmega32U4 and the Arduino analogRead function. Consequently, section headings are more for future reference and for the most part can be ignored if you are reading the material from beginning to end.
Any good story must start with the big picture and ours is no exception. In Figure 3 we have the block diagram of the ADC subsystem of the AVR microcontroller.
Note: ADCSRA ADATE signal mislabeled as ADFR in Figure 3.
Voltage Reference (VREF)
While most of our story centers around how to work with the registers of the ADC. In this section I am going to talk about how the Arduino hardware handles the reference voltage for the 10-bit DAC (AVCC to VREF), how you can improve the noise immunity of your design, and finally how you can change the reference voltage (AVCC to Vref). If you choose to do the latter, make sure you read the warning (or have a spare ATmega32U4 on hand).
The minimum value of the 10-bit output of the ADC register (0x000) represents GND and the maximum value (0x3FF) represents the voltage on the VREF line, within 1 LSB. You can think of VREF as normalizing the input voltage as defined by the following equation.
If you want to get into the details, I would recommend reading “An ADC and DAC Least Significant Bit (LSB)” by Adrian S. Nastase.
The source of the reference voltage is set by bits REFS1 and REFS0 in the ADC Multiplexer Select (ADMUX) register as defined in Table 1.
As shown in Table 1, the reference voltage to the 10-bit DAC can be sourced from an external AREF source, AVCC, or an internal 2.56V reference voltage.
3DoT Hardware Implementation of AVCC and AREF
We are using the 3DoT board and so are limited by how they have wired these pins. Comparing this schematic with Figure 24.7.2 “ADC Power Connections” in the Reference Data Sheet of the ATmega32U4, we see that this is not the optimal wiring solution with missing 100nF capacitor from AVCC to analog ground.
For voltage reference mode 012 and 112 (Table 1) Microchip recommends an external 0.1 uF capacitor be connected from AREF pin to analog ground to improve noise immunity. As seen in Figure 4 “How the 3DoT wires the ADC reference voltage pins” schematic, the 3DoT board comes with with a 0.1 uF capacitor to ground and a Ferrite Bead, the Arduino UNO does not come with either.
Arduino IDE Software Implementation of AVCC and AREF
The default setting for an analogRead call within the Arduino IDE sets ADMUX bits REFS1 = 0, and REFS0 = 1. Looking at Table 1 and the 3DoT schematic (Figure 4), we see that the Arduino IDE therefore defaults to a ADC reference voltage of 3.3v.
Changing the Reference Voltage
The Arduino provides a function named analogReference(uint8_t mode) which allows you to change the DEFAULT value of voltage reference source (REFS = 0b01)
void analogReference(uint8_t mode)
{
// can't actually set the register here because the default setting
// will connect AVCC and the AREF pin, which would cause a short if
// there's something connected to AREF.
analog_reference = mode;
}
Also included in the wiring.h header file are the three available values defined as constants (compare names below to Table 1).
#define EXTERNAL 0 #define DEFAULT 1 #define INTERNAL 3
For example, if you want to reference an external voltage wired to the AREF pin you would add the following code to the setup section of your Arduino sketch. In this example, AREF is wired to the 3.3 V output provided by the Arduino.
analogReference(EXTERNAL); // Vref wired to 3DoT shield AREF pin
WARNING: If you wire a voltage source directly to AREF, when the Arduino changes the ATmega32U4 default setting of 0b00 to the Arduino’s default value of 0b01, a short will exist between AVCC and AREF lines as shown in Figure 3. Even though your setup script will switch it back to its original safe 0b01 value the damage will already have been done to the ATmega32U4 (i.e., time to buy a new microcontroller). To protect the ATmega32U4 during this short period of time, add a 1K ohm resistor between your reference voltage source and the AREF pin. During normal operation the voltage drop across this resistor should be negligible.
How to make an Analog to Digital conversion within the Arduino IDE
Converting an Analog signal into its digital equivalent is accomplished within the Arduino IDE using the analogRead(pins) function. The analogRead function takes a single argument “pin”, identifying one out of the five Analog pins of the 3DoT to be read.
Using the analogRead function is demonstrated by the AnalogInput sketch (Open – Analog – AnalogInput)
/* Analog Input Demonstrates analog input by reading an analog sensor on analog pin 0 and turning on and off a light emitting diode(LED) connected to digital pin 13. The amount of time the LED will be on and off depends on the value obtained by analogRead(). The circuit: * Potentiometer attached to analog input 0 * center pin of the potentiometer to the analog pin * one side pin (either one) to ground * the other side pin to +5V * LED anode (long leg) attached to digital output 13 * LED cathode (short leg) attached to ground * Note: because most Arduinos have a built-in LED attached to pin 13 on the board, the LED is optional. Created by David Cuartielles Modified 16 Jun 2009 By Tom Igoe http://arduino.cc/en/Tutorial/AnalogInput */ int sensorPin = 0; // select the input pin for the potentiometer int ledPin = 13; // select the pin for the LED int sensorValue = 0; // variable to store the value coming from the sensor void setup() { // set pin(s) to input and output pinMode(sensorPin + A0, INPUT); // declare the ledPin as an OUTPUT: pinMode(ledPin, OUTPUT); } void loop() { // read the value from the sensor: sensorValue = analogRead(sensorPin); // turn the ledPin on digitalWrite(ledPin, HIGH); // stop the program for milliseconds: delay(sensorValue); // turn the ledPin off: digitalWrite(ledPin, LOW); // stop the program for for milliseconds: delay(sensorValue); }
In the next section we will look at how the analogRead function works. Our story is about to become a lot darker.
A Simple Analog to Digital Conversion
Before we get into the details of how the Arduino analogRead function works let me give you a thumbnail sketch of the process. If you find yourself getting lost in the forest, you may want to reread the following paragraph. Figure 5 “ADC Registers” is provided here to help you through the mnemonic soup (ADSC, ADCSRA, etc.) contained in this summary paragraph.
The Arduino analogRead function performs a simple analog conversion, where the ADC is triggered manually by setting the ADSC bit to logic one in the ADCSRA register. The ADSC bit will read as logic one as long as a conversion is in progress. When the conversion is complete, it returns to zero. The analogRead function polls this bit and returns the 10-bit result in the ADCH:ADCL register pair when conversion is complete.
Now, let’s take a look under-the-hood at the Arduino analogRead function. I am assuming the use of the Arduino compatible 3DoT board. Consequently, to simplify the explanation of this function I have removed a macro expansion required by the Arduino MEGA.
int analogRead(uint8_t pin) { uint8_t low, high; // set the analog reference (high two bits of ADMUX) and select the // channel (low 4 bits). this also sets ADLAR (left-adjust result) // to 0 (the default). ADMUX = (analog_reference << 6) | (pin & 0x0f); // without a delay, we seem to read from the wrong channel //delay(1); // start the conversion sbi(ADCSRA, ADSC); // ADSC is cleared when the conversion finishes while (bit_is_set(ADCSRA, ADSC)); // we read ADCL first; doing so locks both ADCL // and ADCH until ADCH is read. reading ADCL second would // cause the results of each conversion to be discarded, // as ADCL and ADCH would be locked when it completed. low = ADCL; high = ADCH; // combine the two bytes return (high << 8) | low; }
Exercise 1: The C++ sbi instruction is now deprecated. Write one line of C++ code to replace this instruction.
The Registers of the AVR ADC Subsystem
Although the ADC subsystem of the ATmega32U4 microcontroller contains seven (7) registers (see Figure 5), only ADMUX, ADCSRA, ADCH, are ADCL are used to the configure the ADC as used by the Arduino analogRead function. We will look at ADCSRB, DIDR0 and DIDR2 shortly.
ADC Multiplexer Selection Register Initialization
Assuming the unsigned 8-bit argument pin in the analogRead(pin) function call is zero (0016), the C++ line…
ADMUX = (analog_reference << 6) | (pin & 0x0f);
…at the beginning when the Arduino bootstrap loader initializes the Analog subsystem of the ATmega32U4 microcontroller, the ADC ADMUX register is set to 0100_00002.
Let’s take a closer look at each of these bits and what part they play in our story.
As originally covered in the “Voltage Reference” section of our lesson and summarized in Table 1 “Voltage Reference Selections for ADC”, setting REFS1 = 0 and REFS0 = 1 means that our reference voltage VREF is equal to VCC, which we will assume is 3.3v.
The ADC generates a 10-bit result which is stored in the ADC Data Registers, ADCH and ADCL. By default, the result is presented right adjusted (ADLAR = 0), but can optionally be presented left adjusted by setting the ADLAR bit in ADMUX to logic one. For the analogRead function the result of the conversion process is right adjusted (DEFAULT value).
Exercise 2: Add unsigned 8 bit integer parameter left_adjust to the analogRead function (i.e, analogRead(uint8_t pin, uint8_t left_adjust). Use left_adjust to set or clear the ADLAR bit in ADMUX. Your function should insure all undefined bits of the ADLAR argument (bits 7 to 1) are set to zero.
Using Different Channels as seen from the Figure 3 schematics, the ADC can access multiple analog channels through the MUX. The ADMUX Register controls the MUX and the different input pins which can be directed to the sample-hold circuit. The sample-hold circuit keeps the sampled voltage level stable while the conversion is made, using successive approximation. The analogRead functions pin parameter allows the calling program to define which pin is to be read. The 3DoT board uses the ATmega32U4 processor packaged in a TQFP (See Figure 1-1 “Pinout ATmega16U4/ATmega32U4” in the data sheet). This limits us to 12 analog inputs. Of these 12 possible ADC inputs, the 3DoT makes available 5 (ADC5:ADC0). For my example, the analog signal is read on pin ADC0. Consequently, the input to the MUX is set to 0 (MUX3 = 0, MUX2 = 0, MUX1 = 0, MUX0 = 0).
ADC Control and Status Register A Initialization
During initialization (see init() subroutine in main(void)), the Arduino sets ADCSRA to 1000_01112.
The bits of the ADCSRA register are used to set the operating mode and sample frequency of the ADC. We will look at the “single conversion” and “free-running” modes of operation. These are the two most common operating modes, although there are others. For example you can program the ADC to sample an analog line at a fixed interval of time.
How to select an operating mode
Single Conversion Mode
Before starting a conversion the ADC must be enabled. This is done by setting the bit ADEN = 1 in the ADCSRA Register. As illustrated in Figure 7 this is taken care of by Arduino init() subroutine.
The ADC can be run in two modes: Single Conversion mode or Free-running mode. In Single Conversion mode the conversion is initiated manually, by setting the ADSC bit in the ADCSR Register. Arduino’s analogRead function uses the single conversion mode as shown in the following inline assembly instruction.
// start the conversion sbi(ADCSRA, ADSC);
Free-Running Mode
One of the disadvantages of the Arduino IDE is that we trade flexibility for simplicity. To run the ADC in Free-Running mode it may be best to make your own custom version of analogRead or to write your own ADC C++ within a more powerful IDE like Eclipse, AVR Studio, or ATMEL Studio.
Note: The Analog Comparator Multiplexer Enable ACME bit is not used by the ADC circuit and should be kept at its default value of 0.
In Free-Running mode the ADC is setup to start a new conversion immediately after the previous conversion is complete. To enable this feature, the ADC Auto Trigger Enable ADATE bit in ADCSRA (Figure 7) is set to one and ADC Auto Trigger Source ADTS[3:0] bits in ADCSRB (Figure 8) are set to zero. This selects the ADC interrupt flag ADIF as the trigger source. The ADC now operates in Free Running mode, constantly sampling and updating the ADC Data Registers.
The first conversion is started by writing a logical one to the ADSC bit in ADCSRA. If you use this mode, I would recommend enabling the I-bit in SREG and ADC Interrupt (ADIE = 1) bit in ADCSRA. Now your Interrupt Service Routine (ISR) can take action each time a conversion is complete (no more polling).
3DoT Mode 8
In addition to the free running mode, the ATmega32U4 can automatically start an ADC conversion based on a number of events, as defined in Figure 9 “ADC Auto Trigger Source Selections.” Of particular interest to 3DoT robots is mode 8 “Timer/Counter4 overflow.” The frequency of the PWM signal, which sets the motor speed of a 3DoT robot, is defined by Timer4. Therefore, by using this mode, a 3DoT robot can be taught to read a sensor only when it can take action (i.e., change the speed of its motors). Further, as discussed in Section 24.7.2 “Analog Noise Canceling Techniques” of the ATmega32U4 datasheet, ff any ADC port pins are used as digital outputs, it is essential that these do not switch while a conversion is in progress. Both PWM pins which drive the 3DoT motors are shared with Analog channels, by operating in Mode 8 and assuming a realistic duty cycle (motors not stalled), this restriction can be met.
How to specify resolution/conversion speed (Sample Frequency)
The sample frequency of the AVR ADC is a function of the operating mode (single conversion, free-running mode 8), the ATmega32U4 system clock frequency (CK = 8 MHz), and the prescale setting (ADCSRA bits ADPS2 to ADPS0).
From when a single conversion is initiated to the result is in the ADCH:ADCL Registers 13 ADC cycles will pass as defined in Table 2.
The “First conversion” line refers to when the ADEN bit in ADCSRA register is set. This additional time is required to initialize the analog circuitry including the 7-bit ADC prescaler (see Figure 10 “ADC Prescalar”). The Arduino sets the ADEN bit in the init() subroutine.
By default, the successive approximation circuitry requires an input clock frequency between 50 kHz and 200 kHz to get maximum resolution. If a lower resolution than 10 bits is acceptable, the input clock frequency to the ADC can be higher than 200 kHz to get a higher sample rate. The Arduino init() routine sets prescaler bits ADPS2 to ADPS0 in the ADCSRA Register to all ones (see Figure 7) setting the prescaler to 128. Which means that:
fADC = fCK/128 = 8 MHz/128 = 62.5 kHz
Consequently, the ADC prescaler of the ATmega32U4 generates an ADC clock frequency of 62.5 kHz (less than 200 kHz). By setting the ADC prescaler to 64 (ADPS = 0b110) we can reach a conversion frequency of 125 kHz (again less than 200 kHz), giving us a maximum resolution of 10 bits at a sample frequency of:
fsample = fADC / 13 = 9.615 ksps (104 μs Conversion Time)
This topic is covered in any number of forum posts including “How do I know the sampling frequency?”
Each conversion starts on a rising edge of the ADC clock, and at least one ADC clock cycle is lost executing code. So yes, 8928.6 Hz is the fastest you can get by calling in a tight loop, v.s. a very consistent 9615.4 Hz in free-running mode. – Edgar Bonet Nov 14 ’19 at 14:46
An ADC clock at 250 kHz violates the limit in ADC clocking for full 10 bit resolution (ADC clock 200 kHz for 10-bit resolution). So what would happen if we set our prescaler to divide the system clock by 32 or less? Fortunately, the “Open Music Labs” has done some in depth research and testing to answer this specific question. Table 3 “ATmega ADC resolution versus clock frequency” provides a quick answer; however, I recommend you read the article before trying this at home. From reading the article, you will discover that setting the correct bits in the DIDR register, plays a critical role in improving accuracy.
Working form Table 3, it is shown that a prescaler of eight (8) sets the ADC clock frequency to 1 MHz with a corresponding maximum sample rate of 74.074 ksps (Free-Running mode), with a resolution of an 8-bit ADC.
How to verify conversion complete (polling the ADSC bit)
When we last left our Arduino analogRead routine, it had started the analog to digital conversion process by setting the ADEN bit in the ADCSRA Register to logic one.
// start the conversion sbi(ADCSRA, ADSC);
The analogRead routine now polls the ADSC bit in the ADCSRA register within a C++ while loop.
// ADSC is cleared when the conversion finishes
while (bit_is_set(ADCSRA, ADSC));
Once the conversion is complete, the ADC hardware subsystem of the ATmega328P clears the ADSC bit and stores the result in the ADCH:ADCL register pair.
It is important to read ADCL first to ensure that valid data is read. By reading ADCL first, the ADCL and the ADCH Registers are “locked” and cannot be updated by the ADC until the ADCH has been read.
low = ADCL; high = ADCH;
Our story is finally over, when the high order byte is shifted 8 places to the left and or’d with the low order byte. The 10 bit answer (zero extended to 16 bits) is now returned as a positive integer (16-bit signed).
// combine the two bytes
return (high << 8) | low;
DIDRn – Digital Input Disable Register 0 and 2
Although it does not play a part in the Arduino IDE, your own design should initialize the DIDR0 register, as defined here.
When an analog signal is applied to the digital input buffer of a GPIO port pin, the buffer can consume an unnecessarily high rate of current as the input signal voltage stays within the undefined region between logic one and zero (see slew rate). To solve this problem, pins used as analog inputs should have their corresponding digital pins disabled. Specifically, set the corresponding bit in DIDRn to one. So how does this apply to the 3DoT board?
The 3DoT board can read from 1 to 6 Analog channels as defined in Figure 13 “3DoT Analog Inputs.” Comparing Figures 12 and 13, it is clear that DID2 may be left at its default values (cleared). Conversely, the 3DoT board reads the battery level on ADC0D, DIDR0 bit 0. Consequently, this bit should always be initialized to logic 1. The remaining DIDR0 bits should be set or cleared based on the robot architecture.
Concluding Remarks
As we have seen, the Arduino analogRead function provides a simple way for converting an analog signal into its digital equivalent. In EE470 “Digital Control” the experimentally measured conversion time of the analog_wire_spi.ino script is 125 ?sec (sample frequency = 8 ksps). From our discussion, we know that the conversion time of our Arduino script is 104 ?sec. One thing we can take away from this finding is that over 83% of the time the script is waiting for the ADC subsystem.
Exercise 4: If you replaced the Arduino analogRead function in the analog_wire_spi script, with an interrupt driven routine and placed the ADC in Free-Running mode, what conversion time and sample frequency would you expect to achieve? Hint: see Table 2 “ADC Conversion Time.”
During our journey I hope you have also gained some insights into how we might turbo-charge the little Arduino. For example:
- Switch from Single Conversion to Free-Running mode and moving to an Interrupt driven solution, freeing the processor to do other tasks without sacrificing performance.
- Change your system clock frequency such that the ADC clock frequency can be set to 200 KHz. This will allow you to reach a sample rate of approximately 15 Kbps.
- Overclock the ADC clock to 250 kHz yielding a sample frequency of 19.231 ksps (52 μs Conversion Time).
Finally, the ADC peripheral subsystem of the ATmega32U4 provides a number of features not covered in this introductory material; not the least of which is support for differential inputs and selectable gains.
Review Questions
See Exercises included in the post.
Answers
Solutions to the exercises are to be written.
Serial Communications and SPI
Table of Contents
Reference(s):
The AVR Microcontroller and Embedded Systems using Assembly and C) by Muhammad Ali Mazidi, Sarmad Naimi, and Sepehr Naimi
- Chapter 5: Arithmetic, Logic Instructions, and Programs
- Section 5.4: Rotate and Shift Instructions and Data Serialization
- Chapter 7: AVR Programming in C
- Section 7.5 Data Serialization in C
- Chapter 11: AVR Serial Port Programming in Assembly and C
- Section 11.1 Basics of Serial Communications only (You are not responsible for Sections 11.2 to 11.5)
- Chapter 17: SPI Protocol and MAX7221 Display Interfacing
- Section 17.1 SPI Bus Protocol
Additional Resource Material
- Fairchild Semiconductor MM74HC595 “8-Bit Shift Register with Output Latches” document MM74HC595.pdf
- Arduino Wire Library http://www.arduino.cc/en/Reference/Wire
- Arduino Interfacing with Hardware http://www.arduino.cc/playground/Main/InterfacingWithHardware
Location of Arduino Wire Library C: \Program Files (x86)\ arduino-0017\ hardware\ libraries
Location of #include files stdlib.h, string.h, inttypes.h C: \Program Files (x86)\arduino-0017\ hardware\tools\avr\avr\include
Location of #include file twi.h (1 of 3) C: \Program Files (x86)\arduino-0017\ hardware\tools\avr\avr\include\compat
Introduction
Taking the bits apart and putting them back together again.
ATmega32U4 SPI Features
- Full-duplex, Three-wire Synchronous Data Transfer
- Master or Slave Operation
- LSB First or MSB First Data Transfer
- Seven Programmable Bit Rates
- End of Transmission Interrupt Flag
- Write Collision Flag Protection
- Wake-up from Idle Mode
- Double Speed (CK/2) Master SPI Mode.
What is a Flip-Flop and a Shift Register
You can think of a D flip-flop as a one-bit memory. The something to remember on the D input of flip-flop is remembered on the positive edge of the clock input.
A data string is presented at ‘Data In’, and is shifted right one stage on each positive ‘Clock’ transition. At each shift, the bit on the far left (i.e. ‘Data In’) is shifted into the first flip-flop’s output (i.e., ‘Q’). Source: http://en.wikipedia.org/wiki/File:4-Bit_SIPO_Shift_Register.png
What is a Serial Shift Register with Parallel Load
Setup and Hold Times
- For a positive edge triggered Flip-flop, the setup time is the minimum amount of time before a rising clock edge occurs that a signal must arrive at the input of a flip-flop in order for the flip-flop to read the data correctly.
- Likewise, the hold time is the minimum time a signal must remain stable after the rising clock edge occurs before it can be allowed to change.
- The propagation delay is the maximum amount of time after the positive clock edge to when you can expect to see the signal on the output.
Ten Questions that need answers before you can design a Serial Peripheral Interface
Configuration and Control
- Master/Slave Select Who is the Master and who is the Slave? Specifically, which subsystem contains the clock?
- SPI Clock Rate Select At what clock frequency (divisor) is the data transmitted/received by the Master?
- Data Order In what order is the data transmitted (msb or lsb first)?
- Clock Polarity & Phase How is the data transmitted relative to the clock (data setup and data sampled)
- SPI Slave Select How do you address a slave(s) device?
- SPI Interrupt Enable Will you use polling or interrupts to monitor data transfer operations?
Send/Receive Data
- SPDR Write How do you write data to the SPI Data Register?
- SPDR Read How do you read data to the SPI Data Register?
Monitoring and Status Questions
- SPI Interrupt Flag How do you know when a data transfer operation is done?
- Write Collision Flag How do you detect if a byte of data was written to the shift register during a data transfer operation (read or write).
SPI Overview – Serial Communication
SPI Overview – The Registers
- SPI Control Register – You configure the SPI subsystem by writing to the SPI Control Register (SPCR) and the SPI2X bit of register SPSR. The ATmega328P SPI subsystem may be configured as a master a slave or both. Setting bit SPE bit enables the SPI subsystem.
- SPI Data Register – Once enabled (SPE = 1), writing to the SPI Data Register (SPDR) begins SPI transfer.
- SPI Status Register – The SPSR register contains the SPIF flag. The flag is set when 8 data bits have been transferred from the master to the slave. The WCOL flag is set if the SPI Data Register (SPDR) is written during the data transfer process.
How to Configure the SPI Subsystem
SPI Interrupt Enable
This bit causes the SPI interrupt to be executed if the SPIF bit in the SPSR Register is set and if the Global Interrupt Enable bit in SREG is set. For our design example we will be polling the SPIF bit. Consequently, we will leave the SPIE bit in its default (SPIE = 0) state.
SPI Enable
When the SPE bit is one, the SPI is enabled. This bit must be set to enable any SPI operations.
Data Order
When the DORD bit is one (DORD = 1), the LSB of the data word is transmitted first, otherwise the MSB of the data word is transmitted first. For the Arduino Proto-shield, we want to transfer the most significant bit (MSB) bit first. Consequently, we will leave the DORD bit in its default (DORD = 0) state.
MSTR: Master/Slave Select
This bit selects Master SPI mode when set to one, and Slave SPI mode when cleared. For our design example, the ATmega32U4 is the master and the74HC595 “8-bit Shift Register with Output Latches” is the slave. Consequently, we need to set the DORD bit to logic 1 (MSTR = 1). Note: I am only telling you part of the story. If you want to configure the ATmega328 to operate as a slave or master/slave please see the datasheet.
Clock Polarity and Clock Phase
The Clock Polarity (CPOL) and Clock Phase (CPHA) bits define how serial data is transferred between the master and the slave. These SPI Data Transfer Formats are defined in Figure 10.
SPI Clock Rate Select Bits SPI2X , SPR1, SPR0
These three bits control the SCK rate of the Master. In our design example, the ATmega32U4 is the Master. These bits have no effect on the Slave. The relationship between SCK and the Oscillator Clock frequency fosc is shown in the following table. For our design example we will be dividing the system clock by 16.
SPI Code Example
For our example we are going to use the ATmega32U4 SPI subsystem to send a byte of data to a 74HC595 “8-Bit Shift Register with Output Latches” The interconnection between Master and Slave consists of two shift Registers, and a Master clock generator. For our example the ATmega’s SPI subsystem is the Master and the 74HC595 “8-Bit Shift Register with Output Latches” is the slave.
Overview of the 74HC595
- The 74HC595 “8-bit Shift Register with Output Latches” contains an eight-bit serial-in (SER), parallel-out (QH to QA), shift register that feeds an eight-bit D-type storage register.
- The storage register has eight 3-state outputs, controlled by input line G.
- Separate positive-edge triggered clocks are provided for both the shift register (SCK) and the storage register (RCK)
- The shift register has a direct overriding clear (SCLR), serial input, and serial output (standard) pins for cascading (Q’H).
For our design example, we want data to be clocked into the 74HC595 on the Rising clock edge (the D-flip-flops of the 74HC595 are positive edge triggered), as shown in the Figure below. Consequently, we want the ATmega32U4 to Setup the data on the serial data out line (SER) on the Trailing clock edge. Looking at Table 18-2 we see that this corresponds to SPI Mode = 0.
Configure SPCR
In this example, the SPI peripheral subsystem will be configured to send 8-bits of data a 74HC595. Reviewing the last three pages, to configure the SPI subsystem for the CSULB Shield.
- We will be polling the SPI Interrupt flag (SPIE = 0)
- Enable the SPI Subsystem (SPE = 1)
- Set Data Order to transmit the MSB first (DORD = 0)
- Define ATmega32U4 as the Master (MSTR = 1)
- Configure the SPI to clock data (sample) on the rising edge and change data (setup) on the falling edge. (CPOL = 0, CPHA = 0)
- Set prescalar to divide system clock by 16 (SPR1 = 0, SPR0 = 1)
tl;dr SPCR = 0x51
C Code Example
Assembly Code Example
ldi r16, 0x51 out SPCR, r16
Test Your Knowledge
- The above code assumes that the SPI2X bit in the SPI status register (SPSR) can be left at its default value (SPI2X = 0). How would you explicitly clear this bit in C++ and/or Assembly?
- The above code does not include the instructions to initialize the Data Direction registers for DD_MOSI (Port B bit 3), the SPI clock DD_SCK (Port B bit 5), or our SS signal PB2 (Port B bit 2). How would you write the code in C++ and/or Assembly to initialize the SPI data direction register DDR_SPI (Port B DDR) so these pins were outputs?
How to Operate the SPI Subsystem – Polling –
- Writing a byte to the SPI Data Register (SPDR) starts the SPI clock generator, and the hardware shifts the eight bits into the Slave (74HC595). The Master generates the required clock pulses on the SCK line to interchange data.
C Code Example
/* Start Transmission */ SPDR = data; // data is an 8-bit variable in SRAM
Assembly Code Example
out SPDR,data // data is in register r8
- Data is always shifted from Master-to-Slave on the Master Out Slave In (MOSI) line, and from Slave-to-Master on the Master In Slave Out (MISO) line.
- After shifting one byte, the SPI clock generator stops, setting the end of Transmission Flag (SPIF bit in the SPSR register). If the SPI Interrupt Enable bit (SPIE) in the SPCR Register is set, an interrupt is requested. The Master may continue to shift the next byte by writing it into SPDR. In our lab we used polling to monitor the status of the SPIF flag.
C Code Example
Assembly Code Example
wait: in r16,SPSR bst r16,SPIF brtc wait ret
- After the last data packet is transmitted, the Master will transfer the data to the eight-bit D-type storage register of the slave by strobing the slave select (SS) line. When configured as a Master, the SPI interface has no automatic control of the SS line. This must be handled by your software. Note: We are using the SS line in a non-standard fashion. If you want to configure the ATmega32U4 to operate as a master, slave, or master/slave using the Atmel convention, please see the datasheet.
C Code Example
Assembly Code Example
cbi PORTB,PB0 sbi PORTB,PB0
SPI C++ Code Example
SPI Assembly Code Example
; SPI interface registers .DEF data=r8 InitShield: ldi r16, 0x51 out SPCR, r16 ret WriteDisplay: ; Start transmission of data cbi PORTB,PB0 // ss line active low out SPDR,data rcall SpiTxWait sbi PORTB,PB0 // ss line high ret SpiTxWait: ; Wait for transmission complete push r16 wait: in r16,SPSR bst r16,SPIF brtc wait pop r16 ret
Appendix A Detail Description of the 74HC595
Tri-State Output Buffers
74HC595 Storage Registers (D-Flip Flops)
Review Questions
- TBD
Answers
Using your mouse, highlight below in order to reveal the answers.
- TBD
Serial Communications and I2C
Table of Contents
Reference(s):
- The AVR Microcontroller and Embedded Systems using Assembly and C by
Muhammad Ali Mazidi, Sarmad Naimi, and Sepehr Naimi, Chapter 18 “I2C Protocol and DS1307 RTC Interfacing” - ATMEL 8-bit AVR Microcontroller with 16/32K Bytes of ISP Flash and USB – ATmega32U4, Chapter 20. 2-wire Serial Interface
- Understanding the I2C Bus, Texas instruments Application Note SLVA704-June 2015
- 7-bit, 8-bit, and 10-bit I2C Slave Addressing by Total Phase
- NXP UM10204 I2C-bus specification and user manual
- Serial Communications (I2C and SPI) Eugene Ho, Stanford University
- Arduino Application Program Examples
- Texas Instruments PCA9535 and PCA9555 16-bit I/O port expanders: Arduino I2C Expansion I/O
- LIS3LV02DQ Accelerometer: Arduino and the Two-Wire Interface (TWI/I2C)
Introduction
Many embedded systems include peripheral devices connected to the microprocessor in order to expand its capabilities including
- PCA9564 Parallel bus to I2C bus controller
- PCA9685: 16-channel, 12-bit PWM I²C-bus LED controller
- DS1307 RTC (Real-Time Clock), Textbook section 18.4 “DS1307 RTC Interfacing and programming”
- Adafruit Motor Shield SPI
- HM6352 Digital Compass I2C
- BMA180 Triple Axis Accelerometer SPI or I2C
- ITG-3200 Triple–Axis Digital–Output Gyro SPI or I2C
- GPS module USART
- 1.44″ LCD Display USART
Communication methods can be divided into the two categories parallel and serial. All of these peripherals interface with the microcontroller via a serial protocol. A protocol is the language that governs communications between systems or devices. Protocols may specify many aspects of inter-device communications including bit ordering, bit-pattern meanings, electrical connections, and even mechanical considerations in some cases.
The peripheral device may be completely passive; i.e., there is no controlling mechanism in place within the peripheral or complex enough to include an embedded controller, in which case the protocol may be more sophisticated.
There are many questions that need to be answered when defining a serial communications protocol:
- In what order are the series of bits shifted across the data line? Suppose the master transmits the most-significant bit (MSB) first, then the peripheral will receive the series in the order {0, 1, 0, 1, 0, 0, 1, 1}. Alternatively, the master could transmit the least-significant bit (LSB) first; in which case, the peripheral will receive the series {1, 1, 0, 0, 1, 0, 1, 0}. Either method is fine, but the peripheral and master must agree beforehand; otherwise, incorrect bytes will be received.
- What constitutes a CLK event? The master could use either a falling-edge or a rising-edge clock to specify a sampling signal to the peripheral device.
- How does the peripheral know when it is supposed to receive bytes and when it is supposed to transmit bytes?
All of these questions and many more are answered by the protocol. We have already answered these questions for the SPI interface protocol, now it is time to look at the answers for the I2C interface protocol.
Inter-Integrated Circuit (I2C, IIC, TWI)
Physical Interface
The Inter-Integrated Circuit (I2C or IIC) serial protocol was created by NXP Semiconductors, originally a Philips semiconductor division, to attach peripherals to an embedded microprocessor as shown here. I2C is a multi-point protocol in which peripheral devices are able to communicate along the serial interface which is composed of a bidirectional serial data line (SDA) and a bidirectional serial clock (SCL).
Electrical Interconnection beyond the wires is a single resistor for each of the I2C bus lines. The size of the pull-up resistor is determined by the amount of capacitance on the I2C lines (for further details, refer to I2C Pull-up Resistor Calculation (SLVA689). We will assume a pull-up resistor value of 4.7 kilohm.
The pull-up resistors along with an open-drain bus structure implement a wired-AND.
Source: Wikipedia, Wired logic connection
In a Wire-AND bus structure, to write a ‘0’ the Master or Slave device pull the line low; otherwise the device leaves the line alone to write a ‘1’, which occurs due to the lines being pulled high externally by the pull-up resistor.
Source: Understanding the I2C Bus, Texas instruments Application Note SLVA704-June 2015
Transferring Bits on the I2C bus is accompanied by a pulse on the clock (SCL) line. The level of the data line must be stable when the clock line is high. The only exception to this rule is for generating start and stop conditions.
START and STOP conditions are signaled by changing the level of the SDA line when the SCL line is high.
Transferring Packets. All packets transmitted on the I2C bus are 9 bits long. A packet may either be a device address, register address, or data written to or read from a slave. Data is transferred Most Significant Bit (MSB) first. Any number of data packets can be transferred from the master to slave between the START and STOP conditions. Data on the SDA line must remain stable during the high phase of the clock period, as changes in the data line when the SCL is high are interpreted as control commands (START or STOP).
How Many Slave Addresses?
As defined in the NXP I2C-bus specification and user manual or this handy summary article; the I2C uses 7 bits to address peripheral devices. This means a maximum of 27 = 128 devices can be addressed. However, the I2C specification reserves 16 addresses (0x00 to 0x07 and 0x78 to 0x7F) for special purposes as defined in the following table. Therefore for practical purposes the I2C supports up to 128 – 16 = 112 peripheral devices.
Typical Data Transmission
The general procedure for a master to access a slave device is the following:
Suppose a master wants to send data to a slave:
- Master sends a START condition and addresses the slave.
- Master sends one or more data packet(s) to the slave.
- Master sends a STOP condition, terminating the transfer.
If a master wants to receive/read data from a slave:
- Master sends a START condition and addresses the slave.
- Master sends the address of the register to read from the slave.
- Master receives one or more data packet(s) from the slave.
- Master sends a STOP condition, terminating the transfer.
An address packet consists of 7 address bits (27 = 128 possible addresses), one READ/WRITE control bit and an acknowledge bit. An address packet consisting of a SLave Address and a READ or a WRITE bit is called SLA+R or SLA+W, respectively. See Figure 10.
A data packet consists of one data byte and an acknowledge bit.
The master begins communication by transmitting a single start bit followed by the unique 7-bit address of the slave device for which the master is attempting to access, followed by read/write bit. The corresponding slave device responds with an acknowledge bit if it is present on the serial bus. The master continues to issue clock events on the SCL line and either receives information from the slave or writes information to the slave depending on the read/write bit at the start of the session. The number of bits transferred during a single session is dependent upon the peripheral device and the agreed-upon higher-level protocol.
As an overview discussion of the I2C bus I am intentionally leaving out a number of interesting subjects including:
- Clock Stretching (See reference textbook “Clock stretching” page 636)
- General call address 0000 000
- Receiver sending a NACK after the final byte. To learn more on NACK read Understanding the I2C Bus, Texas instruments Application Note SLVA704-June 2015.
- Master and slave implementing a hand shake to adjust the clock speed to accommodate the slave.
- Multi-master Bus Systems, Arbitration and Synchronization (Reference textbook “Arbitration” page 636, and “TWI Programming with Checking Status Register” page 668)
Overview of the ATmega328P TWI Module
The ATmega328P provides an I2C serial interface via the 2-wire Serial Interface (TWI ) module. The bus allows for up to 128 different slave devices (textbook says 120) and up to 400 kHz data transfer speed. The TWI provides an interrupt-based system in which an interrupt is issued after all bus events such as reception of a byte or transmission of a start condition. The TWI module is comprised of several submodules, as shown here.
All registers drawn in a thick line are accessible through the AVR data bus.
SCL and SDA Pins contain a slew-rate limiter in order to conform to the TWI specification. The input stages contain a spike suppression unit removing spikes shorter than 50 ns. Enabling the internal pull-ups in the GPIO PORT pins corresponding to SCL and SDA can in some systems eliminate the need for external pull-up resistors (20 – 50 kilohm versus 4.7 kilohm specification).
TWI Bit Rate Register (TWBR)
Bit Rate Generator Unit controls the period of SCL when operating in a Master mode. The SCL period is controlled by settings in the TWI Bit Rate Register (TWBR) and the Prescaler bits in the TWI Status Register (TWSR).
Assuming TWPS1= 0 and TWPS0 = 0 (i.e., 4TWPS = 1) , we have a simple way to calculate TWBR as a function of the SCL frequency.
TWBR = ((CPU Clock frequency / SCL frequency) – 16) / 2
For a desired frequency of SCL frequency = 400 KHz
TWBR = ((8 MHz / 400 KHz) – 16) / 2 = 0x02
The Arduino default SCL frequency is 100 KHz as defined here.
#ifndef TWI_FREQ #define TWI_FREQ 100000L #endif
To transmit at 400 KHz you would define TWI_FREQ as 400000L
Note: L is a “literal” meaning Long integer (minimum of 4 bytes = 32 bits).
TWI Data and Address Shift Register (TWDR)
Bus Interface Unit contains the Data and Address Shift Register (TWDR), a START/STOP Controller and Arbitration detection hardware. The TWDR contains the address or data bytes to be transmitted, or the address or data bytes received.
TWI Address Register (TWAR)
Address Match Unit checks if received address bytes match the seven-bit address in the TWI Address Register (TWAR) when the ATmega328P is acting as a slave device. The TWGCE bit enables the recognition of a General Call.
Control Register (TWCR)
Control Unit monitors the TWI bus and generates responses corresponding to settings in the
TWI Control Register (TWCR).
TWINT: TWI INTerrupt Flag
This bit is set by the control unit when requested task is completed and application software response requested.
TWEA: TWI Enable Acknowledge Bit
Generate ACK pulse, if one of three criteria met, .
- Slave address received.
- General call received, while TWAR register’s TWGCE bit is set.
- Data byte received in Master or Slave Receiver mode.
TWSTA: TWI START Condition Bit
Send START condition. Clear when START condition sent (TWINT = 1).
TWSTO: TWI STOP Condition Bit
Send START condition. Automatically cleared when STOP condition sent (TWINT = 1).
TWWC: TWI Write Collision Flag
Indicated write to TWDR when the register is transmitting a byte.
TWEN: TWI ENable Bit
Activate TWI peripheral subsystem. TWI takes control over the I/O pins connected to the SCL and SDA pins.
TWIE: TWI Interrupt Enable
Local TWI interrupt enable bit. If set along with global interrupt bit (SREG I-bit) a TWI interrupt will be requested.
Status Register (TWSR)
When an event requiring the attention of the application occurs on the TWI bus, the TWI Interrupt Flag (TWINT) is asserted. In the next clock cycle, the TWI Status Register (TWSR) is updated with a status code identifying the event.
The TWSR only contains relevant status information when the TWI Interrupt Flag is asserted. At all other times, the TWSR contains a special status code indicating that no relevant status information is available. Status bits TWS7:3 associated with typical I2C serial transmission and reception communication operations without error are defined in the following table and presented in more detail shortly.
Program Examples
AVR TWI in Master Transmitter Operating Mode
In the section entitled “Typical Data Transmission,” I defined three steps for a master to send data to a slave.
- Master sends a START condition and addresses the slave.
- Master sends one or more data packet(s) to the slave.
- Master sends a STOP condition, terminating the transfer.
In the next few sections we will look at a master transmitting a single data packet, following these steps, as a ….
- Timeline
- Flowchart
- C++ Code
After the C++ Code example, we will look at how to implement the I2C using the Arduino wire library.
Timeline
The following timeline shows the interplay of registers comprising the TWA module. A detailed description of the registers and timeline is outside the scope of this overview article and the interested reader is encouraged to read Section 21.6 “Using the TWI” in the ATmega Datasheet.
The TWIINTerrupt (TWINT) flag bit is set by hardware when the TWI has finished its current job and expects application software response. The TWINT Flag must be cleared by software by writing a logic one to it.
The application writes the TWI START condition (TWSTA) bit to one when it desires to become a Master on the 2-wire Serial Bus. The TWI hardware checks if the bus is available, and generates a START condition on the bus if it is free. TWSTA must be cleared by software when the START condition has been transmitted.
Flowchart
Reference Textbook Section 18.5: TWI Programming with Checking Status Register (page 668) Figure 18-18.
C Code
HMC6352 Digital Compass Arduino Example
Source: Compass Heading HMC6352 Sparkfun
I2C HMC6352 compass heading (Sparkfun breakout) by BARRAGAN (http://barraganstudio.com) Demonstrates use of the Wire library reading data from the HMC6352 compass heading.
#include int compassAddress = 0x42 >> 1; // From datasheet compass address is 0x42 // shift the address 1 bit right, the Wire library only needs the 7 // most significant bits for the address int reading = 0; void setup() { Wire.begin(); // join i2c bus (address optional for master) Serial.begin(9600); // start serial communication at 9600bps pinMode(48, OUTPUT); digitalWrite(48, HIGH); } void loop() { // step 1: instruct sensor to read echoes Wire.beginTransmission(compassAddress); // transmit to device // the address specified in the datasheet is 66 (0x42) // but i2c adressing uses the high 7 bits so it's 33 Wire.send('A'); // command sensor to measure angle Wire.endTransmission(); // stop transmitting // step 2: wait for readings to happen delay(10); // datasheet suggests at least 6000 microseconds // step 3: request reading from sensor Wire.requestFrom(compassAddress, 2); // request 2 bytes from slave device #33 // step 4: receive reading from sensor if(2 <= Wire.available()) // if two bytes were received { reading = Wire.receive(); // receive high byte (overwrites previous reading) reading = reading << 8; // shift high byte to be high 8 bits reading += Wire.receive(); // receive low byte as lower 8 bits reading /= 10; Serial.println(reading); // print the reading } delay(500); // wait for half a second }
More Arduino I2C Sketch Examples
Questions
- TBD
Answers
Using your mouse, highlight below in order to reveal the answers.
- TBD
Watchdog Timer
The Basics
Table of Contents
Reference(s):
Material in this document was drawn from these three sources
- Watchdog Timer Basic Example, Written by Nicolas Larsen, 10 June 2011
- ATmega48PA/88PA/168PA/328P Section 10.8 Watchdog Timer (page 50 / 448)
- ATmega16U4/ATmega32U4 Section 8.2 Watchdog Timer (page 48 / 448)
- Standard C library for AVR-GCC avr-libc wdt.h library
- You can find the wdt.h file in the Arduino\hardware\tools\avr\avr\include\avr folder
Introduction
The watchdog timer watches over the operation of the system. This may include preventing runaway code or in our C example, a lost communications link.
The watchdog timer operates independent of the CPU, peripheral subsystems, and even the clock of the MCU.
To keep the watchdog happy you must feed it a wdr (watchdog reset) assembly instruction before a predefined timeout period expires.
The timeout period is defined by a ~128KHz watchdog timer clock and a programmable timer.
Watchdog Timer Reset
“In normal operation mode, it is required that the system uses the WDR – Watchdog Timer Reset – instruction to restart the counter before the time-out value is reached. If the system doesn’t restart the counter, an interrupt or system reset will be issued.” ATmega328P Datasheet Section 10.8.2 Overview
When the Watchdog Reset (wdr) instruction is encountered (pun intended), it generates a short reset pulse of one CK cycle duration. On the falling edge of this pulse, the delay timer starts counting the Time-out period tTOUT.
Watchdog Timer Module
To configure the watchdog timer you define the timeout period by setting the pre-scale value, and define action to be taken if a timeout occurs.
Configuration bits are found in the WDTCSR – Watchdog Timer Control Register. Before you can change the WDE and/or prescaler bits (WDP3:0), the WDCE – Watchdog Change Enable bit must be set.
Define Timeout Period
The WDP3..0 bits determine the Watchdog Timer prescaling when the Watchdog Timer is running. The different prescaling values and their corresponding time-out periods are shown here.
On your own…
How many flip-flops are needed to implement the watchdog prescaler?
Hint: How many bits are needed to generate the longest delay with an input clock frequency of 128KHz?
Define Timeout Action
The Watchdog always on (WDTON) fuse, if programmed, will force the Watchdog Timer to System Reset mode. With the fuse programmed (WDTON = 0) the System Reset mode bit (WDE) and mode bit (WDIE) are locked to 1 and 0 respectively. Arduino / ATmega 328P fuse settings.
The Arduino ATmega328P bootloader sets the fuse to unprogrammed WDTON = 1, which means you can program the action to be taken by setting or clearing the WDE and WDIE bits as shown in the following table.
Note: 1. WDTON Fuse set to “0” means programmed and “1” means unprogrammed.
Watchdog Timer is in Interrupt and System Reset Mode – When the interrupt occurs the hardware automatically clears the WDIE bit, thus putting the watchdog timer into the “System Reset” mode, as defined in the table (WDTON = 1, WDIE = 0, WDE = 1). At the next timeout, a reset is generated.
Watchdog Setup
Turning Off the Watchdog Timer
3DoT Watchdog
Setup
In the watchdogSetup C++ program the WDTCSR register is configured to operate the watchdog timer in the “Interrupt and System Reset” or “Interrupt” mode, with a programmable delay from 1 to 8 seconds.
To configure the WDT a 0x10 WATCHDOG_SETUP command packet is sent with one of the following arguments.
Argument
|
Mode
|
Timeout
|
0x00
|
Watchdog Off
|
|
0x4E
|
Interrupt and System Reset
|
1 sec
|
0x4F
|
2 sec
|
|
0x68
|
4 sec
|
|
0x69
|
8 sec
|
|
0x46
|
Interrupt Only
|
1 sec
|
0x47
|
2 sec
|
|
0x60
|
4 sec
|
|
0x61
|
8 sec
|
0x0E Exception CodesIf one of these arguments is not sent the program sends a 0x0E “Exception” packet with a 0x06 “Watchdog timeout out of range” code. To put this in perspective, here are all the Exception codes and what they mean.
High | Low Order Byte |
01 | Start byte 0xA5 expected |
02 | Packet length out of range 1 – 20 |
03 | LRC checksum error |
04 | Undefined command decoder FSM state |
05 | Array out of range i >= 23 |
06 | Watchdog timeout out of range |
Timeout
If programmed for 8 second “Interrupt and Reset” Mode and a “WDR” command packet is not sent within the timeout period, an interrupt will occur at T+8 seconds and system reset at T+16.
When the interrupt occurs T+8 seconds the hardware automatically clears the WDIE bit, thus putting the watchdog timer into the “System Reset” mode (WDTON = 1, WDIE = 0, WDE = 1).
- A 0x0B “Emergency” telemetry packet with 0x0100 code is sent.
0x0B Emergency Code High Low 01 00 Watchdog timeout
- After this interrupt, at any time (up to T+16) you can reset the timer, turn it off, change modes, etc.
Demonstration
- Plug in an Arduino UNO
- Launch and Configure CoolTerm
- Launch arxrobot_firmware_3DoT
- Normal Operation
A5 02 10 69 DE Set watchdog interrupt for 8 sec A5 01 11 B5 Ping (repeat at a frequency of less than 0.125 seconds) CA 01 11 DA Pong A5 02 10 00 B7 Turn Watchdog Off
• Timeout Example
A5 02 10 4E 59 Set watchdog interrupt for 1 sec CA 03 0B 01 00 C3 Emergency Code 0B, Watchdog timeout 0100 CA 03 06 00 63 AC Read and transmit sensor values after restart CA 03 02 00 00 CB
• Timeout Prescaler out-of-range
A5 02 10 62 D5 ATmega reserved CA 03 0E 06 62 A3 ↓ | ↓ exception | argument error ↓ watchdog timeout out of range
3DoT C++ Watchdog Object
The 3DoT Watchdog object has only one public method
void watchdogSetup(uint8_t);
The 3DoT Watchdog object has two private methods
void watchdogOff(); void throwError(uint16_t);
The 3DoT Watchdog object has three read-only private properties
uint8_t _prescaler; uint8_t _mode; uint8_t _counter;
In the next section we take a closer look at the watchdogSetup method.
3DoT Watchdog C++ Code
Review Questions
- TBD
Answers
Using your mouse, highlight below in order to reveal the answers.
- TBD
Lab 0 – Line Sensing QRE1113 Reflectance Sensor
Table of Contents
Overview
By Miki
IR sensors are commonly used in robotics to detect obstacles and for line following applications. We are using IR sensors for our robots to detect the lines along the 2D maze surface. If our robots can detect a range of grayscale values, so they can navigate the maze. With our current parameters, the IR sensors cannot detect a large range of grayscale values (i.e. low resolution detection).
Goal: Design a circuit that maximizes the resolution of the IR sensors for our robots to navigate through the maze.
The QRE1113 sensor consists of an IR emitting photo diode and an IR sensitive phototransistor. The photodiode emits IR light that gets reflected back to the sensor when an object is in the light’s path.
.
The phototransistor inside the sensor detects the reflected IR light. The base of the phototransistor is open (not connected – floating). When the reflected IR light enters the base of the phototransistor, the light is converted into a base current that controls the collector current of the transistor (EE330 comes in handy here).
The intensity of reflected IR light determines the curve for the current flowing into the collector, as shown in the following graph (Ic vs. Vce). When the intensity of reflected IR light into the base increases, the collector current will increase (i.e. move to a higher curve).
We then place a load line across the graph. The load line represents Vce, the voltage across the
transistor (from collector to emitter). This is the voltage that is read by the ADC in the microcontroller.
We can derive the load line using simple KCL:
Solve for to get the equation of the load line:
In order to maximize resolution, we need to choose the best load line. We therefore need to design a circuit that uses the best value for Rc.
Here is link to the QRE1113 Miniature Reflective Object Sensor used in the SparkFun Analog (Figure 1) and Digital Line Sensor Breakout boards. Here is a short tutorial on the Line Sensing QRE1113 Reflectance Sensor + Arduino , which explains the difference between the Analog and Digital signal conditioning circuits used with the QRE1113. In this lab we will breadboard the “discrete” version of the SparkFun breakout board as shown in Figure 2.
Figure 1 QRE1113 Miniature Reflective Object Sensor
Figure 2 Schematic of SparkFun Analog Line Sensor
Experimentally it has been shown that resistors R1 and R2 used in the Sparkfun “Analog Line Sensor” circuit (Figure 2) do provide a linear output across a uniform grayscale. One of the objectives of this lab is to experimentally determine the optimal resistor values for R1 and R2. To accomplish our objective we will conduct a series of experiments.
From Figure 3 below we see that there are four (4) independent variables
1. Grayscale target
2. Distance to target (variable d )
3. LED current (variable IF )
4. Phototransistor bias (DC load line)
Lab Supplies
● QRE1113 Miniature Reflective Object Sensor (provided)
● 47 ohm resistor and Set of Resistors
● Multi-turn variable resistor – anything above 100 ohms and less than 1K ohm
● Mini , half, or normal breadboard with Jumpers
● Multimeter
● Cables with alligator clips for ECS-314 lab equipment
● Large Post-it Notes or Yellow Pad (may be provided)
● Large Binder Clip(s)
● Hole punch and/or X-Acto knife, a steel ruler, and a cardboard surface to cut on. ( See Note 1 )
● Micrometer (optional)
● Sparkfun ATmega32U4 Pro Micro – 3.3V/8MHz , Arduino Leonardo , or Arduino UNO.
Note 1: One group will be running the labs in ambient light. This group not need the a hole punch or X-Acto knife.
Arduino Code
//Code for the QRE1113 Analog board
//Outputs via the serial terminal – Lower numbers mean more reflected
int QRE1113_Pin = 0; //connected to analog 0
void setup () {
analogReference(EXTERNAL);
Serial.begin (9600);
// See Note 2
}
void loop () {
int QRE_Value = analogRead (QRE1113_Pin);
Serial.println (QRE_Value);
}
*Source: Line Sensing QRE1113 Reflectance Sensor + Arduino
Note 2: As shown in Figure 3 “Experimental Circuit” the circuit is powered from a 3.3v source (Vcc = 3.3v). If you are using a 3.3v Arduino (3DoT or ProMicro 3.3v/8MHz) then no action is required. If you are using a 5v Arduino then power the circuit from the 3.3v output pin and wire a jumper from 3.3v through a 5kΩ resistor to the AREF input pin. Make sure to include the resistor, or you will risk potential damage to the microcontroller. In addition before taking any measurements be sure to set the reference voltage to AREF. This AREF voltage must be present at the reset of the microcontroller and remain the same throughout the experiment. In other words, AREF cannot be set dynamically. Consequently, all analog measurements with be taken with the reference voltage set to 3.3v.
Information about using the analogReference() function can be found here.
Arduino Output using Serial Monitor (Print) and Serial Plotter
.
Excellent Video tutorials on Arduino Print function ( Part 1 and Part 2 ), analogRead ( Part 1 and Part 2 ). Here is documentation on using the Arduino Serial Plotter (Arduino IDE versions 1.6.7 and later).
● Arduino Forum
● Instructables
Bonus Points: You may want to also export your data and plot in Excel or Matlab.
The QRE1113 Sensor Jig
Details
This device was first designed by Roy Benmoshe. It has two main features that make it useful during this lab. First, it blocks out almost all ambient light that could get between the sensor and the surface and possibly skew data results. Its second purpose is to provide a simple, consistent means of adjusting the sensor’s height.
The jig consists of a baseplate supporting a square column. A smaller, four-sided cube slides inside the column. From the picture above, we can see how the cube is able to be slide up and down within the cutout while remaining level. The QRE1113 reflective sensor is positioned so that its viewing window is directly over the small hole in the bottom of the cube.
The sensor has been permanently “potted” inside the square column leaving only four wires exposed. Below the wiring diagram for the jig can be seen.
WARNING: Inaccurately connecting the wires to your circuit could permanently damage and/or destroy the sensor. The voltage source powering your sensor should be 3.3v, NOT 5v. Also be sure to check that you have placed a limiting resistor ( at least 47Ω) in addition to your potentiometer between Vcc and the anode of the LED before any power is introduced to the circuit.
Use
- Place the baseplate onto the testing surface with the sensor facing down
- With the sensor connected to your circuit, slide the small cube containing the sensor into the baseplate (see top right picture above) until both bottom surfaces become flush. Note that the top of the cube will still sit slightly above the top surface of the baseplate (see left picture above).
- Adjust the height of the sensor by sliding the small cube away from the testing surface in 0.2mm increments.
3a. This can be done multiple ways however, the easiest way we found was to cut a square section (roughly 1.5 cm X 1.5 cm) out of an entire standard pad of sticky-notes. Note this section must be smaller than 2 cm X 2 cm, so it can fit inside the square column.
3b. This sticky-note pad can then be placed between the small cube that houses the sensor and the testing surface. You can then push slightly on the baseplate, so its bottom face returns to the testing surface and the cube is displaced the exact distance of the sticky-note pad thickness.
3c. Once the cube has been displaced to the appropriate height, the sticky-note pad is removed and, due to the friction between the cube and the baseplate, the displacement distance will remain the same.
3d. The distance of the displacement (i.e. distance between the sensor and the surface) can be adjusted by peeling off a predetermined number of sticky-note sheets (1 sheet ≃ 0.09 mm). For this reason it would most likely be easiest to start with the maximum height desired (5 mm) during the test, then peel off sticky notes in 0.2 mm intervals until the minimum desired height was reached.
Experiment 1 – Optimum Distance to Target
.
1. Grayscale target – White Paper
2. Distance to target – independent variable [0.2mm steps, 0 – 5mm ]
3. LED current = 20 mA
4. Phototransistor bias (DC load line) = 10K
Experimental Setup
As discussed in lab. You will need the following.
● Notebook paper
● Voltmeter and variable resistor – record voltmeter accuracy
● Arduino microcontroller board (3DoT, Sparkfun ProMicro 3.3v, Leonardo, Uno)
Important Notes:
- As shown in Figure 3 the circuit is powered from a 3.3v source (Vcc = 3.3v). If you are using a 3.3v Arduino (3DoT or ProMicro 3.3v/8MHz) then no action is required. If you are using a 5v Arduino then power the circuit from the 3.3v output pin and wire a jumper from 3.3v to the AREF input pin.
- All experiments should not allow room lighting to reach phototransistor.
Experimental Output
For each experimental step d = 0 – 5mm, in ≃ 0.2mm steps take readings using a voltmeter and as read from the Arduino serial output port (analogRead ).
Solving the equation provided in the Analog-to-Digital lecture and provided here for VIN , convert the Arduino ADC output (units are a Digital Number – DN) into its corresponding voltage ( VIN ).
, where = 3.3v
Using Ohm’s law, and measured value for R2 (≃ 10KΩ), calculate Ic . On one graph using two different notations (ex. X = Voltmeter, O = Arduino) plot your experimental results. Using Excel, Matlab, etc. curve fit your experimental results for both sets of readings (again on a single plot). Your resulting plot should take the form shown in Figure 4. Differences from this figure include IF = 20mA, VCE = 3.3v, TA = measured; Two plots (Voltmeter, Arduino) and no “Mirror” plot; Actual current (not Normalized).
Based on team assignment set d = [Optimum, 1, 2, 2.5, 3, 3.5, 4] mm
Experiment 2 – Optimum LED Current and R1
.
1. Grayscale target – White Paper
2. Distance to target d – Based on team assignment
3. LED current = independent variable IF = 0 – 40 mA, in 2 mA steps
4. Phototransistor bias (DC load line) = 10K
As in most engineering problems there is a tradeoff to be made. In this case we want to use the minimum amount of power, while also achieving the best resolution possible.
Experimental Output
For each experimental step IF = 0 – 40 mA, in 2 mA steps take readings using a voltmeter and as read from the Arduino serial output port (analogRead). Following the procedures outlined in Experiment 1, create a plot, which should take the form shown in Figure 5. Differences from this figure include Two plots (Voltmeter, Arduino).
As an ancillary objective measure the forward voltage ( VF ) drop at different forward current values ( IF ). For each experiment record the room temperature.
Based on IF current assigned [5, 10, 15, 20] to the team find R1
Experiment 3 – Optimum R2 based on Team Assignment
.
1. Grayscale target – White Paper
2. Distance to target d – Based on team assignment
3. LED current IF – Based on team assignment
Experimental Objective
One of the key objectives of this lab is to experimentally generate Figure 4 “Collector Current vs. Distance” between device and target and Figure 8 “Collector Current vs. Collector to Emitter Voltage” as shown in the QRE1113 Miniature Reflective Object Sensor datasheet.
The characteristic curve for the Phototransistor (Figure 8) will be generated from a gray scale as provided here. Base on experimental results you may be required to focus on additional gray scale images.
Figure 7 Sample Gray Scale Page
Experimentally, once an optimum distance has been experimentally determined, the transistor characteristic curve will be generated using a load line for each gray scale target (see Figure 9 ”Sample transistor characteristic curve with load line”).
Use the above information and your EE330 “Transistor Characteristic Curve Lab” to complete this lab. As before please collect data using a multimeter and your Arduino.
Based on experimental results calculate R2
Reference Lab Experiments
● Photo Transistor Characteristics by Dr. Gabriel M. Rebeiz
● Lesson 1452, Optoelectronics Experiment 6, Photodiode and Phototransistor Current Measurements
● http://inst.eecs.berkeley.edu/~ee105/sp11/labs/Lab3.pdf
● http://archivio.iav.it/Doc/Vanni_Paolo/5CNT/EE332LE1rev3.pdf
Lab Report
Introduction
The introduction essentially summarizes the entire experiment. It defines the purpose of the lab and explains the goals and/or what one is trying to learn in the process. It should also present any concepts (scientific theories, previous experiments etc.) that act as background information, which would be crucial to understanding the lab.
Materials/Setup
This section should, as the name implies, include a list of all materials used to complete the lab. It will also explain how each of these items were prepared in order to obtain the results of the experiment. It is important to be as detailed as possible here, both to eliminate procedural ambiguity and to increase the lab’s ability to be reproduced.
For this lab specifically, all three experiments will share a materials section however, each will have its own setup procedures. Also, many students have found it easier to include pictures of their setup with only a brief description.
Discussion
The discussion is the main section of the lab report, where the data from the experiment is presented and both it and the lab itself are reviewed. This includes any challenges encountered during the procedures, as well as reaction to the data. Interpretations of the results themselves should be avoided in this section, and discussion should instead be limited to answering questions such as how successful the experiment was and how the data compared to the expected outcome.
In this specific lab, experimental data must be presented using graphs. Since there are three separate parts to this experiment, there should also be three subsections under the main discussion section, each presenting its corresponding data. Graphs can be generated in a number of ways however, the most common forms seen in this class are MATLAB and Microsoft Excel.
Conclusion
The conclusion section elaborates on the claims made during the discussion. It seeks to provide an explanation as to why the results appear the way they do, and whether or not they make sense in the context of the initial goal of the experiment. It is important to state if the goal was achieved, as well as what was learned during the course of the lab.
Appendix
The appendix section is where all code used during the lab should go. For this experiment, that includes both the Arduino code for reading the sensor, as well as any code used to generate the graphs (MATLAB, Python etc.). Each block of code should have a corresponding subsection within the appendix. For example, all of the Arduino code might be labeled as subsection “A”, while the entire MATLAB code could be labeled as “B”.
.
.
Lab 0 Deliverable(s)This lab requires a full, written lab report. Only one report per team is required, which will be submitted via the “Dropbox” folder in Beachboard. The outline and requirements for this report can be found above in the section labeled “Lab Report”. All lab reports should represent your own work – DO NOT COPY. |
Checklist
- Your lab report follows the format listed above under the “Lab Report” section, including an abstract, introduction, materials/setup, discussion, conclusion and an appendix
- Graphs have been generated for each of the three experiments
- A graph of “IC vs VCE” is included
- A graph of “IC vs Distance” is included
- A load line is included on the appropriate graph
Appendix
Appendix A – Source Material
C++ Robot Lab 1 – An Introduction to 3DoT & C++
Table of Contents
Introduction
This lab training sequence is designed to introduce you to the 3DoT Board. the Arduino Integrated Development Environment (IDE), C++ programming, and the methodology used to navigate the maze. Plus, you will learn about the power of library files. Library files are simply files that you instruct the Arduino IDE to include in your program. In this lab, you are going to create a library file called 3DoTConfig.h that handles all of the initialization and configuration for your robot.
What Is New
New terminology and concepts are listed below under “Bit/Byte Operations” and “Pin Configuration & Control”. If you have any questions on this information, refer back to the lectures on the introduction to C++ and configuring the GPIO registers.
C++ Essential Training (Required)
This Linda tutorial provides a comprehensive review of the basics of C++.
Getting Started in Embedded Systems (Required)
This Linda tutorial offers a nice introduction to C++ and embedded systems.
Please download certificates when you have completed the tutorials and submit them on Beachboard.
Data Types
uint8_t variable // Define variable data type as an unsigned 8 bit integer (0-255) uint16_t variable // Define variable data type as an unsigned 16 bit integer (0-65,535) int8_t variable // Define variable data type as a signed 8 bit integer (-128 to 127) int16_t variable // Define variable data type as a signed 16 bit integer (-32,768 to 32,767) const uint8_t variable // Qualify variable as a constant static uint8_t variable // Qualify variable that will persist after a function return
C++ Code
Preprocessor Directives
#define Name Text // Replace every occurrence of Name with Text. #include // Include avr/io.h library located in the Arduino program folder #include “filename” // Include library located in the sketch (i.e., program) folder
Bit / Byte Operations
variable = byteValue << numberOfShifts; // Left shift byte value by specified number of bits. variable = _BV (bitNumber); // Create a byte with specified bit set to 1. variable |= _BV(bitNumber); // Set specified bit within a byte variable. variable &= ~_BV(bitNumber); // Clear specified bit within a byte variable variable = !another_variable // C++ Boolean NOT operator
Arduino Built-In Functions
Pin Configuration & Control
pinMode(pin_number, TYPE); // Define digital pin and set type as INPUT, OUTPUT, or INPUT_PULLUP digitalRead(pin_number); // Read digital value from specified digital INPUT or INPUT_PULLUP pin. digitalWrite(pin_number, value); // Write digital value to specified digital OUTPUT pin.
3DoT Overview & Mission
The end goal of these labs is to program a robot utilizing the 3DoT board and be able to navigate a maze autonomously. Shown below are the major features of the 3DoT board and the block diagram of the latest version.
3DoT is a micro-footprint 3.5 x 7 cm all-in-one Arduino compatible microcontroller board designed for robot projects.
- Microcontroller: ATmega32U4
- Bluetooth: FCC-certified BLE 5.0 module
- Power Management:
- RCR123A battery holder
- Included 600 mAh rechargeable battery
- Microchip MCP7383 battery charge controller
- External battery connector – for input voltages between 4 – 18 V
- Reverse polarity protection – plug in the battery backward? No problem
- Motors & Servos:
- 2x JST motor connectors
- 2x standard servo connectors
- Expansion:
- 16-pin top female headers for shields – providing I/O, I²C, SPI, USART, 3.3 V, and 5 V.
- The forward-facing 8-pin female header for sensor shields – providing 4 analog pins, I²C, and 3.3 V power – for sensor shields like infrared or metal-detecting shields. Great location for headlights, lasers, ultrasonics, etc.
- Programming switch: Three-position switch for easy programming
- No more double-tapping a button and rushing to program your board, or your robot trying to drive away while programming. Set the switch to PRG to program, RUN to execute your code.
It would be ideal to spend some time analyzing the block diagram to get a better understanding of the capabilities of the 3DoT board but we will focus on the connections between the motor driver and IR sensors for Lab 1. Figure 3 provides an isolated view of those connections.
3DoT Block Diagram v10 3DoT v10 Detail Block Diagram 201215
.
Errata: AIN1, AIN2/PWMA,BIN1,BIN2/PWMB,NSLEEP wired to 5V_VM
The main objective of Lab 1 is to develop the path following logic for your robot to move through the maze shown in Figure 4 “The Maze.” We will handle what to do at intersections in later labs. The path following algorithm reads data from two of the four IR sensors and decides how the control signals being sent to the motor driver need to change based on that information. The outside IR sensors provide the most relevant data about your robot’s position as it follows the path and is what we will be focused on. You can read the two inside sensors to verify the robot has not left the path. To start developing this code, you will need to configure the input and output pins.
Introduction to the Arduino IDE
In the lab, you will be spending most of your time working within an Integrated Development Environment (IDE). For our labs, we will be working in the Arduino IDE. The IDE lets us write our program in a human-readable form (C++) and then translate it into a machine-readable form understood by the ATmega32U4. There are many other types of IDEs for C++ programmings such as Eclipse and Atmel Studio 7 that allow us to create very complex projects. The focus will be on the Arduino IDE because of how easy it is to learn and use.
Before continuing complete the ARDUINO IDE 3DOT TUTORIAL, up to and including and running the BLINK test script.
Creating A New Sketch In Arduino IDE
To start, create a new sketch in the Arduino IDE. When the Arduino IDE launches, it should open a blank sketch. If you do not see a blank sketch, click File > New as shown in the figure below.
The first thing you will be adding is a title block to describe the purpose of the sketch and the author. This will go before the setup function and should look something like this.
Next, you will need to rename the sketch to a more descriptive name. Click on File -> Save and type in the name you want to save it as such as Lab01. The application will create a folder for your sketch in the location you have designated for all sketches (Default is in the Arduino folder located in My Documents). Now let’s take a look at the hardware before developing the code to control it.
Making the 3DoTConfig.h Library File
Libraries allow us to place useful definitions and initialization code outside our main program. Once created and included, the user can still call these functions and reference these definitions in their main .ino file. Libraries make the main program much cleaner and simpler to understand.
A C++ library file typically includes a header file (.h) that provides the prototypes of the functions in the library and a .cpp file that has all of the C++ code. For Lab 1 the library header file will include the actual C++ code and be used to configure the robot. First, a new header “.h” file will need to be created. Click on the icon and select “New Tab” as shown in Figure 7 below. Name the file 3DoTConfig.h
Simply copying and pasting the following HTML text may insert hidden characters into your code, resulting in multiple compiler errors. The author recommends manually typing in the following material.
Place the following include directives at the beginning of your sketch. The quotes (“) around “3DoTConfig.h” instructs the application to look in the folder containing the program (your .ino file).
#include "3DoTConfig.h"
void setup() {...
Using #define to Create Descriptive Names
The #define preprocessor directives (or macro) instructs the compiler to replace an identifier with its associated text string.
Unlike C++ instructions, preprocessor directives do not end with a semicolon (;).
For this series of lab, the #define preprocessor directive, associates an Arduino pin name with its 3DoT equivalent as defined in Figure 3 “Reflective IR Sensor to Motor Driver.” For example, this #define macro will replace every occurrence of the text string IR_R_O (i.e., the outside IR sensor trace) with the text string A0 (i.e., it Arduino pin name).
#define IR_R_O A0 // IR_R_O = analog pin A0
Place the #define macro preprocessor definitions for IR sensor traces IR_R_O, IR_R_I, IR_L_I, and IR_L_O in the newly created 3DoTConfig.h file.
The same can be done for the control signals to the motor driver so that each motor and its associated pins can be clearly distinguished from the other. Add #define macro preprocessor definitions for AIN1, AIN2, NSLEEP, BIN1, and BIN2.
In addition, define synonyms PWMA and PWMB.
// Synonyms #define PWMA AIN2 #define PWMB BIN2
To provide visual feedback for future tests, we will be using the built-in LED wired to ATmega32U4 Port D pin 5. Physically, the LED is located between the switch and the micro USB port (Figure 1). See if you can find the built-in LED on the schematic in Figure 2 “3DoT Detailed Block Diagram.” Add a #define preprocessor directives for the built-in LED.
#define LED_BUILTIN PD5 // Built-in led is wired to ATmega Port D pin 5
Unlike the previous definitions which used the Arduino naming convention. The LED_BUILTIN definition uses the ATmega32U4 naming convention.
An alternative approach
For digital pins, this can also be done by creating a variable that is set to the pin number. Since these values will never change, a const qualifier is added. This new form has the advantage that it allows the compiler to check scope and datatype. It has the disadvantage that it uses SRAM to save these constants. Our first #define example could therefore be replaced with the variable definition
const uint8_t IR_R_O = A0;
In this instance, the author recommends the #define directive over a constant variable based on the savings in RAM.
Quick Check
The 3DoTConfig.h file should contain twelve (12) #define preprocessor directives.
Configuring the Input and Output Pins
The first thing that needs to be done is to define what pins are going to be used for the Atmega32U4. Just because components have been hardwired to specific pins does not mean the microcontroller knows what they are being used for. You will need to configure the input and output pins. In the next section, the Arduino’s built-in functions are used to configure the input and output pins. In the design challenge, you will apply what you learned in the lecture to directly modify the GPIO registers (DDRX and PORTX) in C++.
Add the following function definition to the 3DoTConfig.h file after the last # defines statement.
void init3DoT() { // add code here };
Place the following line of code in the setup() function.
void setup() { init3DoT(); }
In a previous section, pins were assigned to the IR shield and motor driver. Once the Arduino preprocessor knows the name of a pin, the program can configure the pin as an input or output, using the pinMode() function. Once a pin has been configured, the program can read or write to the pin using the Arduino digitalRead() and digitalWrite() functions.
Initialize the Motor Driver
Start the initialization of the ATmega32U4 by defining Motor Drive pin AIN1 as an output and initializing it to zero (OFF).
void init3DoT() { pinMode(AIN1,OUTPUT); // Digital pin AIN1 is an output digitalWrite(AIN1,LOW); // Left Motor OFF }
Following the above example, configure motor driver pins AIN2, NSLEEP, BIN1, BIN2 as outputs and initialize to zero.
Initialize the IR Sensors
Next, initialize the right outside IR sensor as a digital input.
void init3DoT() { pinMode(AIN1,OUTPUT); // Digital pin AIN1 is an output digitalWrite(AIN1,LOW); // Left Motor OFF : pinMode(IR_R_O,INPUT); // Analog pin A0 }
Following the above example, configure IR pins IR_R_I, IR_L_I, IR_L_O as inputs.
Initialize the Built-in LED
Finally, initialize the Built-in LED pin as an output and initialize to zero. The Arduino reserved words LOW and HIGH are of datatype uint8_t and have values 0x00 and 0x01 respectively. Also note that like LOW and HIGH, LED_BUILTIN is colored cyan.
pinMode(LED_BUILTIN,OUTPUT); digitalWrite(LED_BUILTIN, LOW); // Turn the built-in led OFF
Reading and Interpreting the IR sensor Data
The IR sensors provide feedback as the robot follows a line. In this section, you will learn how to read the IR pins as digital inputs.
As a digital input, the input value read from the IR sensor when it is over a black line is HIGH (a value of 1), otherwise, it is LOW.
white | 0 |
black | 1 |
This is determined by the microprocessor’s own threshold for digital logic and cannot be modified. While there is no customization, it is very useful for situations that have only two states and is easy to implement. Within the Arduino IDE, this is done using the digitalRead()function.
IR Sensor Test
Before continuing, let’s do a quick check to see if the IR sensors are working. This test will also use the BLINK led to provide visual feedback as you debug your code. Place the following test code in the loop() section of the program.
void loop(){ int val = digitalRead(IR_R_O); // read IR sensor digitalWrite(LED_BUILTIN, val); // display on blink led }
To run the test you will need to upload the code to the robot. This is a two-step process after the hardware has been prepared. The first thing that needs to be done is to verify the code by clicking on the checkmark on the top left corner (the verify button). This will compile and check the code to see for any syntax errors. Any error messages will appear in orange at the bottom of the window and will need to be fixed before the program can be uploaded to the board. Once verified, upload the compiled code to the board using the right arrow button (upload button) that is next to the verify button. If everything went correctly, the led should turn OFF when the right outside IR sensor is over a white or reflective surface and ON otherwise. Tip: Placing and then slowly lifting your finger off the IR sensor will also turn it on and off.
Test the operation of the other IR sensors by replacing IR_R_I with the names of the other IR sensors (i.e, IR_R_O, IR_L_I, and IR_L_O), uploading, and rerunning the test.
Debugging Tips
If the output is always 1, this may be due to the sensor’s height off the path or the IR LED not being ON. First, try placing your sensor closer to the line. If this does not work, verify that the IR LED is ON by looking at the sensor with your cell phone camera. Some cell phone cameras have an IR filter that can block the light. Consequently, if your camera initially does not see the LED try another phone. If you still can not see the IR LED verify that the shield is powered (3.3v).
If the output is always 0. Stray light is probably saturating the photo-transistor. This problem may be solved by simply adding the paperBot shell or taping paper around the boot to block the light from entering the sensor.
If none of the above work, remove the IR shield and using a resistor and/or jumper, wire the analog pin to 3.3V and GND, and verify built-in LED turns ON and then OFF.
Controlling the Motors
Wiring the Motors
Now let’s teach the robot how to move forward. Verify that the robot’s motors are wired correctly as shown in the PaperBot Chassis Build Instructions video and Figure 8 “Wiring the Motors.”
Motor Control Settings
Figure 9 “3DoT Pinout and Motor Driver Truth Table” is a great resource that you will be using over the course of the semester.
Use the following truth table when programming your robots.
Action | Input | |||
Left Motor | Right Motor | |||
AIN1 | AIN2 | BIN1 | BIN2 | |
Motors OFF | 0 | 0 | 0 | 0 |
Forward | 0 | 1 | 0 | 1 |
Spin Right | 0 | 1 | 1 | 0 |
Spin Left | 1 | 0 | 0 | 1 |
Reverse | 1 | 0 | 1 | 0 |
Enable the Motors
In the “Configuring The Input and Outputs Pins” section you defined the ATmega32U4 pins controlling the motors as outputs using the Arduino pinMode() function. Whenever you define an output pin you must initialize it to some value. For the motors, this was done by setting all outputs to LOW using the Arduino digitalWrite() function or by modifying the PORTD and PORTB registers (design challenge). In other words, the robot was placed in a safe state (no running away). In this section, you teach the robot to move forward by editing the value parameters of the digitalWrite(pin, value) function calls.
Moving Forward
Add an initialization section to loop.
void loop() { /* Initialization */ }
Add two (2) Arduino digitalWrite() functions to the Initialization section of loop(). Reading from Table 3 configure the robot to move forward (AIN1, BIN1). Now let’s do a quick check to see if our motors work. Add the following test code to the loop() section of your program.
void loop(){ /* Initialization */ : /* Test code */ digitalWrite(PWMA, HIGH); digitalWrite(PWMB, HIGH); }
Verify that both PWM pins have been configured as outputs within the /*Initialization*/ block.
pinMode(PWMA,OUTPUT); // PWMA is mapped to AIN2
pinMode(PWMB,OUTPUT); // PWMB is mapped to BIN2
Upload the code to your robot. If everything went correctly, your robot should start to run away.
Quick Check
At this point in the lab the init3DoT() function should be complete; containing ten (10) pinMode and six (6) digitalWrite instructions. Specifically, all ATmega32U4 IR sensor inputs and motor outputs should be configured (Figure 3).
The initialization section of the loop() function should contain four (2) digitalWrite instructions.
Path Following Algorithm
Now that you have the robot moving forward, let’s incorporate the IR sensors to provide feedback as your robot tries to follow a path. Replace your test code in loop() with the following code after your digitalWrite() function calls in the initialization section.
void loop() { /* Read the IR sensors */ int sensorLeft = digitalRead(IR_L_O); // White = 0, LOW int sensorRight = digitalRead(IR_R_O); // Black = 1, HIGH /* Run the path following algorithm */ int motorLeft = !sensorRight; // we invert and cross the wires int motorRight = !sensorLeft; /* Write to the motors */ digitalWrite(PWMA, motorLeft); digitalWrite(PWMB, motorRight); }
How it Works
The first block reads the two outer IR sensors as digital inputs. In fact, the output from these sensors is an analog signal. As digital signals, anything below 0.56 V will be interpreted as logic 0 (LOW) and above 1.56 V as logic 1 (HIGH). These are in compliance with the LVTTL JEDEC standard and defined in Section 29.2 DC Characteristics of the Atmel-7766J-USB-ATmega16U4/32U4-Datasheet_04/2016 Document. Clearly, these may not be the optimal threshold settings for these IR sensors. In addition, when an analog signal drifts between these two threshold values, the input transistors can inject noise into the chip, which can result in excessive current consumption, heating, and ultimately failure of the device. So why did we do this? The short answer is to keep this early lab as simple as possible. A more technically correct answer is that the ATmega32U4 includes input circuitry to protect against drifting signals from damaging the device. This potential problem will be corrected by reading each IR signal as an analog input in Lab 3.
The second block of code runs the line following algorithm. To help understand how this works, visualize the robot traveling down the path. When the robot starts to veer off the path towards the left, it means that the right motor is rotating faster than the left motor. This may be due to differences in friction within the gearbox and/or the DC motor. At some point, the left IR sensor will detect the black line on the left side of the path, and sensorLeft = 1. In order to adjust and get the robot back on the path, the algorithm will stop the right motor (motorRight = 0) until the left outside IR sensor is back on the white surface of the maze. In summary, when input sensorLeft = 1, we want the output motorRight = 0 and visa-versa, something easily accomplished using the C++ Boolean NOT operator (!) and swapping the inputs. The same logic applies when it veers to the right. While this simple algorithm is good enough for following a path and stopping at an intersection, it is not intelligent enough to teach the robot how to actually take a step (takeAStep) or turn in the maze. This problem will be addressed by writing a more advanced algorithm in Lab 4.
Like the IR sensor, using binary signals to control the motors (ON/OFF) is sub-optimal. So why did we do this? The short answer is again to keep this early lab as simple as possible. This problem will be corrected by writing a Pulse Width Modulated (PWM) signal to the motors, approximating an analog output in Lab 2.
Design Challenge
You can skip the remainder of this lab if you are happy receiving a passing or even a good grade on the lab. If you want to receive an excellent grade you will need to accept the challenge(s). Specifically, the maximum grade you can receive on the prelab 1 plus lab 1 if you do not accept the challenge(s) is 20 points out of 25 (80%).
Working with the GPIO Registers
The Arduino IDE is designed for non-engineers and therefore, as much as possible, it hides what is going on within the MCU. As engineers, we would like to understand how the MCU really works and for this lab how the registers within the General Purpose Input/Output (GPIO) peripheral subsystems can be programmed to work with digital pins.
In order to configure a pin as an input, the corresponding bit position in the DDRX register needs to be cleared to 0. If that input pin needs to use a pull-up resistor, the corresponding bit position in the PORTX register needs to be set to 1. Obtaining the value that is being detected on that input is done by reading the corresponding bit position in the PINX register. For an output pin, the value in the DDRX register needs to be set to 1. You have to write the value that is to be outputted in the PORTX register. This information is summarized in Table 4.
From figure 5 you will notice that our input pins for the IR sensors are PF7, PF6, PF5, and PF4. The output pins that go to the DRV8848 motor driver are PB7, PD7, PC7, and PB6. It is possible to define each pin individually but it would be more efficient to develop an expression to get the final value that needs to be written to each 8-bit register. It is also important to preserve the values of the other bits in the GPIO register because that would change the functionality of those pins. This can be done with a combination of the bit/byte operations that were introduced earlier.
As an example, let’s set Port B bits 6, 5, and 4 to logic 1 without modifying the other bits in PORTB. If you were to configure them individually, you might start with the following instruction.
The right-hand side of the expression can be read as; take 0b00000001 and shift it to the left 4 times, where PB4 is defined as equal to ‘4’. The resulting binary value is 0b0001000. You can say the same thing by using the bit value macro _BV(bit), included in the avr/io library. A bit-wise OR (=|) is then performed with DDRB and this binary value. Recalling that 1 OR anything is 1 and 0 OR anything is anything, this expression will set only bit position 4, defining that pin as an output.
That right-hand side of the equation can set a single bit, or again by application of the bit-wise OR operation, multiple bits. This means we can set DDRB bits 6, 5, and 4 in a single line of code.
For clearing values, the following instruction would be used. It is performing the bit-wise AND operation on the original value of DDRD and the complement of the expression.
The ChallengeTurn the Arduino functions you wrote in 3DoTConfig.h into Comments (//), and replace them with their C++ equivalent. One advantage of working directly with GPIO port registers (DDRX and PORTX) is that one line of C++ will replace multiple Arduino lines of code, which work with one pin at a time. |
Lab 1 Deliverable(s)
All labs should represent your own work – DO NOT COPY. Submit all of the files in your sketch folder. Make sure that the code compiles without any errors. Do not forget to comment your code. Lab 1 Demonstration
|
Checklist
Remember before you turn in your lab…
- The configuration of the robot is written in the 3DoTConfig.h file.
- All of the configuration is done in the init3DoT() function.
- The init3DoT() function should be called in the setup() function.
- The robot should move forward and use the IR sensors to follow a line
C++ Robot Lab 2 – Motor Control and Fast Pulse Width Modulation
Table of Contents
Introduction
This lab is designed to introduce you to how pulse width modulation (PWM) is used to control the speed of an electric motor, how the timers of the Atmega32U4 can be configured for fast PWM mode to produce a desired PWM signal, and how to apply that to your line following algorithm.
What is New?
C++ Flow Control
To the C++ terminology and concepts learned in Lab 1; Lab 2 adds flow control using if-else statements. Before you begin I would recommend watching Lynda’s Fun with C++, part of the Learning C++ lecture series.
if (condition) { statement block } else { statement block }
Arduino Built-In Functions
To the terminology and concepts learned in Lab 1; Lab 2 adds the following Arduino Built-in function. If you have any questions on this information, refer back to my lecture on Pulse Width Modulation and Lynda’s lecture on Arduino Pulse Width Modulation. If you decide to do the first of the recommended “Power Tips” you should also read up on Arduino Serial Communications.
analogWrite(pin_number, PWM-value); // Define timer pin and output PWM digital waveform. Serial.println(val, format); Serial.read();
Improving Control of the Motors
Up to this point, you have been simply turning the motors ON and OFF. This is not the best solution for following a path and results in the robot ping-ponging from one side of the path to the other or more likely driving off the path entirely. In this lab, our main goal is to get better control of our robot by applying what we learned in the Pulse Width Modulation lecture.
Using the Timers in Fast PWM Mode
As you learned in the lecture, the timers can be configured to generate the desired PWM values and output them on specific pins. Those pins are indicated in Figure 1. If you plan to use this feature for the timers, you must use one of the five available PWM pins. You will notice that we are already using PD7 and PB6 (Arduino pins D6 and D10) for our PWM 3DoT v10 Block Diagram control signals for the motor driver.
Errata: AIN1, AIN2/PWMA, BIN1, PIN2/PWMB, NSLEEP wired to 5V_VM
The Timer with PWM lecture goes into detail about how this entire system works and you should understand that before we move forward. For the actual implementation, you can use the built-in function of analogWrite(). All of the configurations that were described in the lecture are handled for you in the analogWrite() function and the specific limitations are described here.
takeAStep
Open Lab1 in the Arduino IDE. Save As Lab 2. At the end of loop(), create a new function named takeAStep(). Move your path follower code into this new function. To the now-empty loop() function add a call to takeAStep(). Your program should now look something like this.
void loop() { takeAStep(); } void takeAStep() { // path follower code from Lab 1 }
At the present time, your robot can follow a path. You may have also discovered that it also slows down at an intersection. This follows from your algorithm which stops a motor when its associated sensor is over a line. At an intersection, both sensors at some point will detect a line and therefore stop their respective motors momentarily, with the other motor taking the robot over the line.
If you have not done so already, verify that your robot slows down at an intersection.
Find the Maximum Speed
There is a high probability that your two motors are not equal. This is typically due to a difference in the internal friction of the gears and motors, which translates into a difference in the speed of rotation at a given voltage or duty cycle. This will cause your robot to veer towards the right or left. In order to address this issue, we will start by finding the ideal maximum speed for the fastest motor such that the robot travels in a straight line (or at least as close as possible).
This portion of the lab involves a lot of trial and error. With a pencil and yardstick, make a light straight line on a white surface that is about three feet long (i.e., the back of your maze). You will be manually controlling the speed of each motor using the analogWrite() function.
Temporarily comment out the call to takeAStep() and add analogWrite() functions to control the motors.
Begin with both motors running at full speed (PWM value of 255) and record in which direction the robot drifts from the path. If it drifts to the right, then the left motor is faster than the right. Decrease the speed of the faster motor by lowering the duty cycle (PWM) of the faster motor. Repeat this process until the motors are in sync. Define the maximum speed for both motors as a constant integer in the 3DoTConfig.h file.
const uint8_t motorRightHIGH = ____; // Synchronize motors max const uint8_t motorLeftHIGH = ____;
Power Tips
If you want to save time you can also write a bit of code and simply use the Serial.read() and Serial.println() functions to find the maximum PWM value for the faster motor. As an alternative, you can use a potentiometer with the wiper (middle terminal) connected to analog pin A4 wired to connector pin 6 on 3DoT Header J5 and the outside leads to 3.3V and ground (GND) wired to connector pins 3 and 4 on 3DoT Header J2, you can find the maximum and minimum speeds quickly by adapting the example script provided for the analogWrite() instruction.
Find the Minimum Speed
The objective of this section is to find the minimum speed that will keep the motors synchronized and provide a safe margin above the stall speed for both. A motor will stall when there is insufficient power to the motor to overcome the internal friction of the gears and motor (i.e., nothing is moving). The motor is ON at this point and it is consuming power but there is no physical motion. This may damage the motor and should be avoided. By obtaining the minimum speed to get the motor moving, we can keep the motor ON and barely moving while the robot smoothly gets back on track.
Using trial-and-error determine the minimum speed for each motor. Specifically, slowly decrease the duty cycle (PWM value) until the minimum speed is reached with a margin of safety. If you want to save time you can again write a bit of code and simply use the Serial.read() and Serial.println() functions to find this minimum safe PWM value or use a potentiometer as described in the previous section.
Next, run both motors at their minimum speed and note how the robot deviates from the line. Increase the speed of the slower motor. Repeat this process until the motors are in sync. Define the minimum speed for both motors as a constant integers in the 3DoTConfig.h file.
const uint8_t motorRightMin = ____; // Synchronize motors min const uint8_t motorLeftMin = ____;
We will be experimentally determining the motorRightLOW and motorLeftLOW constants in the next section.
A More Majestic Step
Now that you have identified all of the values that are needed to synchronize the speed of the motors, you can update the path following the algorithm to have the robot follow a path with fewer corrections. In a perfect world, the robot should move forward and never veer off a straight line with this change. We do not live in a perfect world.
Delete the analogWrite() functions and restore the call to takeAStep() in the main loop.
void loop() { takeAStep(); }
/* Write to the motors */
In takeAStep(), replace the digitalWrite(PWMA, motorLeft); and digitalWrite(PWMB, motorRight); functions with the corresponding analogWrite() calls.
/* Write to the motors */
analogWrite(PWMA, motorLeft);
analogWrite(PWMB, motorRight);
Integer variables motorLeft and motorRight, which in lab 1 were assigned values of simple LOW (i.e., 0) and HIGH (i.e., 1) will now contain PWM values between 0 and 255.
/* Run the path following algorithm */
Replace the two (2) assignment statements in the “Run the path following algorithm” block with if-else conditional expressions setting each motor’s max (motorLeftHIGH, motorRightHIGH) and min (motorLeftLOW, motorRightLOW) PWM value as a function of its’ corresponding sensor input value (sensorLeft, sensorRight).
/* Run the path following algorithm */
if (condition) {
// Statement(s) to execute if condition is true
} else {
// Statement(s) to execute if condition is false
}
:
Binary Search Algorithm
In lab 1 we simply changed direction by turning a motor off. In this section, we want to experimentally determine the speed to set the motor that was previously turned off to a value that will make the most gentle correction possible. If the value is too low then the robot will continue to ping-pong as it moves along the line; too high and the robot could not correct its’ course in time and may leave the path. Once experimentally determined these values will be assigned to integer constants motorLeftLOW and motorRightLOW in the 3DoTConfig.h file. These constants will have values between their HIGH and MIN values. The fastest way to experimentally determine the optimal speed is to use a binary search algorithm. Start by setting the motor LOW constants to a value between their HIGH and LOW values.
guess low = (max + min)/2
const int motorRightHIGH = ____; // Synchronize motors max const int motorLeftHIGH = ____; const int motorRightMin = ____; // Synchronize motors max const int motorLeftMin = ____; const int motorRightLOW = ____; const int motorLeftLOW = ____;
Upload and run the line following code. If the robot leaves the path then you need to decrease the LOW value. Guess a value between your last guess and the minimum value. If the robot stays on the path then you need to increase the LOW value. Guess a value between your last guess and the maximum value. Repeat these steps until you find the speed that results in the most gentle correction possible without leaving the path. If you want to save even more time you can again write a bit of code and simply use the Serial.read() and Serial.println() functions or use a potentiometer as described earlier.
Design Challenge – pwmWrite
You can skip the remainder of this lab if you are happy receiving a passing or even a good grade on the lab. If you want to receive an excellent grade you will need to accept the challenge(s). Specifically, the maximum grade you can receive on lab 2 if you do not accept the challenge(s) is 20 points out of 25 (80%).
The Arduino IDE is designed for non-engineers and therefore, as much as possible, it hides what is going on within the MCU. As engineers, we would like to understand how the MCU really works and for this lab how the registers within the Timer 4 peripheral subsystems can be programmed to replace the analogWrite() functions.
Open the 3DoTConfig.ino tab and add the following function definition.
void initTimer4(){ // add code to initialize timer 4 here. };
Add a call to the initTimer4() function to the init3DoT() function.
void init3DoT() { // initialization code from lab 1. initTimer4(); }
Write a function named pwmWrite(motor, value) to replace your analogWrite(pin, value) calls. Place this function after the takeAStep() function definition.
Lab 2 Deliverable(s)
This lab will be turned-in with Lab 3. |
C++ Robot Lab 3 – Sensor Data as an Analog Input
Table of Contents
Introduction
While a single binary number (1 or 0) makes it easy to make decisions (true or false) in your C++ code, you must rely on the digital input logic to determine what analog voltage will be a interpreted as a 1 and or a 0. This may not result in the best choice for your IR sensors which output an analog value. Now that you have gotten the robot to move forward we will use the IR sensors analog inputs to make decisions.
What Is New
C++ Data Type Qualifier
The static qualifier instructs the compiler to save a variable into SRAM memory and not to destroy it at the end of a block of code; where a block of code is defined by curly brackets {}.
static datatype var;
Arduino Built-In Functions
To the terminology and concepts learned in the previous labs; Lab 3 adds the following Arduino Built-in function. If you have any questions on this information, refer back to the lecture on Analog-to-Digital Conversion.
analogReference(type); // DEFAULT=3.3V, INTERNAL=2.56V, EXTERNAL=AREF analogRead(analog_pin); // Read 10-bit analog value.
Find HIGH and LOW
The analog-to-digital converter (ADC) peripheral subsystem of the ATmega32U4 translates an analog voltage on an input pin into a digital number within the range of 0 to 1023. Within the Arduino IDE, this is done using the analogRead()function.
To convert an analog voltage into a digital number (DN) the ADC needs a reference voltage. By default, the reference voltage is equal to VCC (i.e., 3.3V). In most cases, the voltage from the IR sensors will not exceed 2 V. Therefore, to increase the range of DNs read, we will switch our reference voltage from 3.3V to a 2.56V reference source generated internal to the ATmega32U4.
Once converted to a 10-bit unsigned integer, your challenge is to create an algorithm to determine what that value represents (HIGH or LOW). For example, if the value is less than 50, it could mean that you are detecting the white part of the maze. If the value is greater than 50 but less than 700, it could be some shade of grey (you are on the border of the line). If it is above 700, it could mean that black is detected and you are over the line.
Using the Serial.println() function and the following test script, determine the threshold values that will work best for your robot. Try viewing the output both on the text-based Tools > Serial Monitor and graphical Tools > Serial Plotter
You will find the color key for the four (4) plots on the upper left-hand side of the graph.
void setup(){
Serial.begin(9600); // setup serial
analogReference(INTERNAL); // reference voltage = 2.56v
}
void loop(){
uint16_t val = analogRead(analogPin); // read the input pin
Serial.print(analogRead(IR_R_O));
Serial.print(" ");
Serial.print(analogRead(IR_R_I));
Serial.print(" ");
Serial.print(analogRead(IR_L_I));
Serial.print(" ");
Serial.println(analogRead(IR_L_O));
delay(50); // waits for 50 milliseconds
}
sensorRead()
Open Lab2 in the Arduino IDE. Save As Lab3. Here is a template for your to be written sensorRead() function.
uint8_t sensorRead(uint8_t sensor_pin, uint8_t current_level) { 1. read analog value 2. conditional expressions set return logic_level based on analog value read and trigger points. 3. if the analog value is in the undefined region (no mans land) then logic_level equals the current_level argument. return logic_level; // return HIGH or LOW }
The sensorRead function returns HIGH if the analog value is greater than or equal to trigger1, and LOW if the analog value is less than or equal to trigger0. When the analogRead() function returns any value that is neither (gray area), your function should return the current logic level (HIGH or LOW). This is known as hysteresis and will keep your robot from reacting too quickly to changes in the input (noise). To implement hysteresis, your function receives as its’ second argument (current_level) the last returned value.
To determine my trigger points I followed these steps.
- hysteresis = (high value – low value)/2
- trigger1 = high value – hysteresis/2
- trigger0 = high value + hysteresis/2
For my IR sensors when over black the recorded digital numbers were very volatile but usually over 300. When over white the recorded value was fairly stable at 79. Although, if I simulated going over a bumpy surface this value could spike up wildly. So a hysterics value of 221 DN seemed like a reasonable number with trigger1, therefore, set to 245 and trigger0 set to 135. Based on your experiments, you may want to implement a different set of criteria for setting your trigger points and possibly even use the runningAverage script found in the StatPak tab to remove high-frequency noise from your sensor readings. Open the 3DoTConfig.h tab and set the trigger points where an analog reading; a digital number (DN) between 0 and 1023 is translated to a digital HIGH or LOW value
/* IR sensor constants */ const uint16_t trigger1 = ____; // IR values 1023 to ____ = 1 (black) const uint16_t trigger0 = ____; // IR values ____ to 0 = 0 (white)
Return to the home tab. If you have not done so already, complete writing the sensorRead() function.
To maintain compatability with the Arduino digitalRead() function, your sensorRead() function will return a data type uint8_t with values HIGH or LOW. If you are converting your Arduino script to a C program, then I would recommend switching to a bool datatype, with a value of true or false.
Now that you have identified your trigger points and written the sensorRead() function, you are ready to update the “Read the IR sensors” block within takeAStep to improve your robot’s ability to follow the path.
In takeAStep() within the “Read the IR sensors” block, replace the digitalRead() Arduino functions with your new sensorRead() function. Add this new function at the end of takeAStep(). Your program should now look something like this. Observe that sensorLeft and sensorRight are both arguments sent to and returned from sensorRead. Therefore, they must be defined as static uint8_t data types before the call to sensorRead. The subject of the next section.
void loop() { takeAStep(); } void takeAStep(){ /* Read the IR sensors (lab 3) */ uint8_t sensorLeft = LOW; // initial value white, motor ON uint8_t sensorRight = LOW; sensorLeft = sensorRead(IR_L_I,sensorLeft); // White = 0, LOW sensorRight = sensorRead(IR_R_I,sensorRight); // Black = 1, HIGH /* Run the path following algorithm */ Remainder of path follower code from Lab 2 }
A Memory Bug
The sensorRead function takes as its second argument the current value of the sensor (current_level). To add hysteresis to the function, we simply return this value, if the sensor reading (analog_value) does not meet the HIGH or LOW threshold values. So for our solution to work, the variables sensorLeft and sensorRight need to be remembered between calls to takeAStep. Unfortunately, that is not how local variables work. To save memory, local variables are destroyed at the end of the subroutine. If you want the subroutine to remember the value of a local variable you need to add the static qualifier as shown here.
void takeAStep() { /* Read the IR sensors */ static uint8_t sensorLeft = LOW; // initial value white, motor ON static uint8_t sensorRight = LOW; :
Design Challenge
You can skip the remainder of this lab if you are happy receiving a passing or even a good grade on the lab. If you want to receive an excellent grade you will need to accept the challenge(s). Specifically, the maximum grade you can receive on the prelab 3 plus lab 3 if you do not accept the challenge(s) is 20 points out of 25 (80%).
In lab 3 you have the opportunity to accept one or both of the following challenges. Completing one challenge may result in a perfect score. Accepting a second challenge will be rewarded with bonus points. Bonus points are added to your overall lab grade in the course. Your overall lab grade may not exceed 100%. In other words, by accepting a second challenge you may be able to skip a future challenge or make-up for points lost in previous labs.
Design Challenge 1 – getAnalog()
Write new functions initADC() and getAnalog(). The initADC() function should be called from setup() and initialize the ADC peripheral subsystem of the ATmega32U4. The getAnalog() function should replace all Arduino analogRead() function calls. Both functions must be written in C++ (no Arduino functions).
Design Challenge 2 – Interrupt Service Routine
Write an interrupt service routine (ISR) to read the analog signal from the IR sensors, replacing the existing ADC polling based solution. The solution, should operate the ADC in “3DoT Mode 8” as defined in the “C/C++ Arduino Robots” class lecture on “Analog to Digital Conversion.” In addition the initialization routine should disable the corresponding digital input pins, shared with the IR sensors, as discussed in the “DIDRn – Digital Input Disable Register 0 and 2” section of the same lecture.
Design Challenge 3 – PID Controller
Please talk to the instructor before attempting this design challenge. This challenge may be completed at any time during the semester.
Apply material in the PID Controllers lecture to add a PID controller to help your robot follow the line. In other words, you will now work with the native analog value of the sensor inputs (i.e., not digital values). The error input to the PID controller should be the difference between the analog readings between the left and right sensors.
Lab 3 Deliverable(s)
All labs should represent your own work – DO NOT COPY. Submit all of the files in your sketch folder. Make sure that the code compiles without any errors. Do not forget to comment your code. Lab 3 Demonstration
|
C++ Robot Lab 4 – Take A Step
.
Table of Contents
Introduction
Your robot can now follow a path. In this lab, we are focusing on developing the code to detect a room boundary and continue until the robot’s wheels enter the next room (hop). When this lab is completed, you will be ready to develop the control algorithm for turning in the maze. The critical issue for the takeAStep algorithm is keeping track of the robot’s position with the sensor data that is available. This requires readings from the sensor(s) and implementation of a Finite State Machine (FSM) using a C++ switch-case statement. Your FSM will have three (3) states.
- walk
- hop
- stop
What is New
C++ Code
switch(expression) { case constant: statement(s); break; case constant: statement(s); break; ... default: //optional statement(s); }
Arduino Built-In Functions
millis() // number of milliseconds since program started running.
Walk
Open Lab3 in the Arduino IDE. Save As Lab 4. Create a new function named walk. The walk will allow us to reduce the path following code to only the two lines as shown. For reference, my lab 3 “path following algorithm” originally used 16 lines (9 C++ statements) of code.
void takeAStep() { /* Initilization */ : /* Read the IR sensors */ : /* To follow a path the ir sensor controls the opposite motor */ analogWrite(AIN1, 0); // left motor analogWrite(AIN2, walk(sensorRight,motorLeftHIGH, motorLeftLOW)); analogWrite(BIN1, 0); // right motor analogWrite(BIN2, walk(sensorLeft,motorRightHIGH, motorRightLOW)); /* Write to the motors */ : } int walk(uint8_t sensor, uint8_t motorHIGH, uint8_t motorLOW) { uint8_t motorSpeed; : return motorSpeed; }
The new walk function accepts three arguments and returns the PWM signal to be sent to the left or right motor (motorLeft or motorRight). The first walk argument is sensor which is either the left or right sensor reading (LOW or HIGH). With one exception; the structure of walk is functionally equivalent to your original algorithm, with the sensor value used in a conditional expression setting the speed (motorSpeed) of the opposite motor to its corresponding motorHIGH (parameters motorLeftHIGH, or motorRightHIGH) or motorLOW (parameters motorLeftLOW, or motorRightLOW) value accordingly. The one exception is how the new walk function, operates at the end of a room. Before the robot would only slow down as it passed over a room boundary. We now want to detect a room boundary using one of both of our inner IR sensors and once detected pass over the room boundary and then stop inside the next room.
Before you continue, verify that your robot’s performance has not changed.
Build the Framework
To take a step, we are going to upgrade our path following algorithm to a three (3) state, finite state machine (FSM). We will be using a C++ switch-case statement to implement the FSM. An if-else-if construct could have been used just as easily. A simple switch-case contains a variable (the switch) which is compared to a series of constants (the cases). The block of statements within a case is run on a match. For reasons, that date back to antiquity, statement blocks after the case are also run. To prevent this unfortunate behavior case statements nearly always end with a break instruction.
void setup
void setup(){ : delay(5000); // 5 second delay to set robot in the maze } : void takeAStep() { /* Initialization */ : /* Finite State Machine */ static int state = 0; // fsm state /* Read the IR sensors */ : /* Run the path following algorithm */ switch(state) { /* follow path until room wall (i.e., inner IR) detected */ case 0: // walk statements break; /* move forward (open loop) until inside room case 1: // hop statements break; case 2: // stop } // end switch /* Write to the motors */ : }
Text in gray is from the previous labs. Let’s take a closer look at each section.
/* Initialization */
As you write your code for each case, within the line following the algorithm you may need to declare a few new variables. Place these declaration statements in the initialization section of takeAStep.
/* Finite State Machine */
The beginning of the finite state machine section initializes the FSM and switches state to zero.
/* Run the path following algorithm */
To complete a single step the FSM sequentially moves through two states (or cases), to be defined in the following sections.
- state 0: walk
- state 1: hop
- state 2: stop
Variables sensorLeft,sensorRight,motorLeft, and motorRight typically hold the input (sensor) and output (motor) values for each case.
state 0: walk
Write the code needed to teach your robot to walk until the end of a room is reached; the inner sensor(s) reads white. Before the end of a room is reached you can simply re-purpose your original walking code. This is the path following block-of-code from Section 3 without the definition of motorLeft and motorRight (both were defined as integers in the header block). Unlike the previous version, where your robot kept walking down the hallway, instruct your robot to proceed to the next state.
if (inner sensor(s) in the room){ // walking code : } else { // start hopping analogWrite(AIN1, 0); // "true" A ==> left motor (simply a convention) analogWrite(AIN2, motorLeftHIGH); analogWrite(BIN1, 0); // right motor analogWrite(BIN2, motorRightHIGH); state = 1; }
state 1: hop
At the end of the last state, the motors were set to their ON position. During the hop state, your robot should proceed in an open-loop fashion. Continue doing nothing, until the inner sensor(s) detects the next room (white).
if (inner sensor(s) in the next room){ switch to state 2 }
state 2: stop
At the end of the last state, the motors were still in their ON position. In this final state, end the mission by turning off the motors (brake) and turn ON the LED_BUILTIN led.
analogWrite(AIN1, 255); // left motor analogWrite(AIN2, 255); analogWrite(BIN1, 255); // right motor analogWrite(BIN2, 255);
Test Your Code
Upload your code. Verify that the robot follows the path and hops into the next room – BLINK led turns ON.
Lab 4 Deliverable(s)All labs should represent your own work – DO NOT COPY. Submit all of the files in your sketch folder. Make sure that the code compiles without any errors. |
Checklist
Remember before you turn in your lab…
- FSM sketch with explanatory comments.
C++ Robot Lab 5 – Turning Algorithm
Table of Contents
Introduction
Your robot can now follow a path and stop at the start of a room (takeAStep) by implementing a simple algorithm that controls the motors based on inputs from the sensors. In this lab, we are focusing on how to turn the robot to face the desired direction. When this lab is completed, you will be able to control your robot for all situations it can encounter in the maze and will be ready to develop the control algorithm for navigating your path through the maze in Labs 6, 7, and 8. The critical issue for the turning algorithm is keeping track of the robot’s position with the sensor data that is available. This requires continuous readings from the sensor(s) and implementation of Finite State Machines (FSMs). Along the way we will write three (3) new functions:
- turnLeft()
- turnRight()
- turnAround()
What is New
Data Types
const uint8_t variable // Qualify variable as a constant static uint8_t variable // Qualify variable that will persist after a function return
C++ Code
do { statement statement . . . statement } while (expression); // Repeat while expression remains true
Serial Monitor Commands
Serial.begin(9600); // Configure baud rate Serial.print(“text”); // Print the text to the serial monitor Serial.println(variable); // Print the variable to the serial monitor and start a new line
Timing Control
delay(time_value); // Delay program execution by the time_value milliseconds
Build the Framework
Open Lab4 in the Arduino IDE. Save As Lab 5. At the end of takeAStep() add the following three (3) functions.
void loop() { takeAStep(); } void takeAStep(){ // code from lab 1, 2, 3 and 4 } void turnLeft(){ /* Initialization */ /* Turn algorithm */ /* Write to the Motors */ } void turnRight(){ } void turnAround(){ } int sensorRead(int sensor_pin, int current_level){ // code from lab 3 }
takeAStep
You have now completed the education of your robot in how to take a step. Now it is time to teach it how to turn. Unfortunately, although yes, your robot can take a step it will not play nice with any new functions called from within loop. To solve this problem, in this section you will “encapsulate” the takeAStep function within a do-while loop.
The takeAStep() function works because it is continuously called from within loop. put another way, we are calling and returning from the takeAStep() function until the step is completed. This causes a problem if we want to add additional functionality to our robot. In this instance functions turnLeft, turnRight, and turnAround – and that is the problem. If for instance, we were to follow takeAStep with turnRight, the program would run the line following algorithm once, then run the turnRight once, resulted in a really confused robot. Before we solve the problem, lets take a closer look at what is happening. If we were to tunnel down into the C++ code which implements the loop method from within the Arduino scripting language, we would find this while statement.
while(1) { loop(); }
Because the while condition is always true (1), the statements within the while block, in this case loop(), will be repeatedly called until the Arduino is reset. And here in lies the solution to our problem. To keep our takeAStep function from returning, we can “encapsulate” it within its’ own loop. However, in place of a condition which is always true, we will add a condition that will allow our function to return when the robot has completed a single step. Lets take a look at the solution.
void takeAStep() { /* Initialization */ : /* Finite State Machine */ static int state = 0; // fsm state do { /* Read the IR sensors */ : /* Run the line following algorithm */ switch(state) { : /* Write to the motors */ analogWrite(PWMA, motorLeft); analogWrite(PWMB, motorRight); } while(state != 0); }
The do-while looping construct works best for our solution, because we want to make at least one pass through the FSM. On that first pass we move from state 0 to state 1, this means that the while test will fail (we will loop) until state 3 is completed and we return to state 0.
Add the do-while loop to the takeAStep() function.
Test Your Code
Before you added the do-while loop, your robot could take a step. Your robot should now be able to do exactly the same thing, so how do you know if your program is working? The answer is pretty simple. Move your test code in state 2: // hop, which turns ON the BLINK led after your call to takeAstep(). If everything is working the led will stay OFF until the robot completes the first step and then will turn ON. Once you know your code is working you may comment out (//) or delete the test code.
Configure Motors
It is finally time to start teaching your robot how to turn. First, review “Controlling the Motors” in Lab 1. Up to this time, the DRV8848 motor driver has been configured for the right and left motor to move the robot forward. In the Initialization section of functions turnLeft() and turnRight(), configure the motors for the desired turn as defined in Table 1 “Motor Control Setting.”
Action | Input | |||
Left Motor | Right Motor | |||
AIN1 | AIN2 | BIN1 | BIN2 | |
Motors OFF | 0 | 0 | 0 | 0 |
Forward | 0 | 1 | 0 | 1 |
Spin Right (CW) | 0 | 1 | 1 | 0 |
Spin Left (CCW) | 1 | 0 | 0 | 1 |
Reverse | 1 | 0 | 1 | 0 |
Brake | 1 | 1 | 1 | 1 |
Spin Test
Unless you add a sensor (one of the design challenges) your robot will be executing an open-loop turn. Consequently, your robot will need to be able to turn about a point without drifting in any one direction. The tuning of the motors to eliminate drift is the objective of the spin test.
In setUp(), comment out takeAStep()and add a call to turnLeft(). Add the following code to turnLeft().
void turnLeft() { /* Initialization */ : /* Turn algorithm */ : /* Write to the motors */ analogWrite(AIN1, motorLeftHIGH); analogWrite(AIN2, 0); analogWrite(BIN1, 0); analogWrite(BIN2, motorRightHIGH); }
Compile and upload your code. Your robot should start spinning counter-clockwise. If your robot can complete at least two (2) revolutions without drifting then you can skip to the next section, otherwise, you will need to tune your motors employing the techniques learned in Lab 2 “Motor Control and Fast Pulse Width Modulation.”
Replace motorLeftHIGH and motorRightHIGH with numerical values that keep your robot spinning in place. To discover these numerical values, using one of the techniques you employed in Lab 2, to tune the motors to track in a straight line, teach your robot to spin in place. These techniques included: trial-and-error, wheel encoder, a potentiometer, and binary search.
If you are unable to tune your robot to spin about a point, you may need to implement one of the hardware sensor solutions discussed in the design challenge section of the lab.
Finite State Machine Turning
In Lab 4 you implemented a four (4) state FSM to teach your robot to walk, hop, and skip. In this lab, you will again implement a four (4) state FSM to teach your robot how to turn. Implement the following pseudo-code. I would recommend looking to your takeAStep FSM for help.
/* Turn left algorithm */ switch (state) { case 0: /* hold in reset state for some period of time */ delay(2000); // robot moved to maze state = 1; // continue to state 1 break; case 1: /* loop on this state until right sensor enters the room in front of the robot */ case 2: /* continue rotation through the room */ case 3: /* rotation is complete when the right sensor enters the room to the left of the robot */ } // end switch
Once you have your robot turning left, encapsulate your finite state machine within a do-while loop. Enable (remove comments) takeAStep and run your code. Your robot should now take a step and then turn left. After a short delay between actions (takeAStep – turnLeft – takeAStep …) your robot now autonomously walks around in circles (well actually squares). should traverse a square.
Repeat the steps above to teach your robot to turn right.
Turn Around
After testing your robot; determine in which direction it most reliably turns (left or right). Call the corresponding function twice within turnAround() to make a U-turn.
Lab 5 Design Challenge
You can skip the remainder of this lab if you are happy receiving a passing or even a good grade on the lab. If you want to receive an excellent grade you will need to accept the challenge(s). Specifically, the maximum grade you can receive on the prelab 2 plus lab 2 if you do not accept the challenge(s) is 20 points out of 25 (80%).
These challenge may be completed at any time up to the last day of class.
Wheel Encoder
- If your robot includes a wheel encoder, implemented your turn using the encoder and the pin_change() function. The pin_change() function is included with the StatPak.ino file.
- Replace software polling-based pin_change() function with a hardware pin change interrupt.
If you had trouble tuning your robot to spin about a point without drifting you may want to accept this challenge. Specifically, you are going to add a sensor to help your robot spin about a point. The following list contains only a few of the many possibilities in order to point you in the right direction. It is important to note that using a combination of more than one sensor for your algorithm greatly improves accuracy/functionality.
- Rotary Encoder (not a wheel encoder)
- Used to determine the position of a rotating shaft (usually a motor), through either a magnetic or optical sensor
- Here is a basic tutorial from the Arduino website including example code
- Note: pull up resistors are commonly required for certain types of rotary encoders
- Gyroscope
- Measures 3D angular velocity
- IMU (Inertial measurement unit)
- Usually, a combination of an accelerometer and a gyroscope together, although it can include a magnetometer
- Measures both acceleration and angular velocity
- Requires complex mathematical algorithms to process data
- When used correctly, can provide very detailed position information
- Magnetometer
- Measures magnetic fields (commonly Earth’s magnetic field)
Howtomechatronics has a useful tutorial on these concepts
*There are many more possibilities than the options listed here, and creativity will be rewarded.
Lab 5 Deliverable(s)All labs should represent your own work – DO NOT COPY. Submit all of the files in your sketch folder. Make sure that the code compiles without any errors. |
Checklist
Remember before you turn in your lab…
- FSM sketch with comments.
- The sketch of your own implementation.
Trash
Tip: An intersection is present when both sensors return 1 (true). You may also want to review C++ logical operators.
So how does this work? The first line creates a Boolean variable named start with an initial value of true. The statements within the do-while loop, which teaches the robot how to follow a line, will therefore be executed until an intersection is detected at which time the loop condition is set to state = 0 and the program returns to the Arduino loop method.
Write the Algorithm
Translate your algorithm into code using C++ conditional expressions. Start by watching this tutorial to learn/review C++ conditionals. Place your code in the main loop() section of your Arduino program.
Hint: The program can be implemented using a number of methods including:
- Binary operators (|, &, <<, >>, ~)
- If-else statement
- Conditional operator (?)
- Arduino map() function
You have now completed the education of your robot in how to take a step. Now it is time to teach it how to turn. Unfortunately, although yes, your robot can take a step it will not play nice with any new functions called from within loop. To solve this problem we will “encapsulate” the function within a within a do-while loop.
before we even start we will need to encapsulate takeAStep() within a do-while loop so we can add functions turnLeft, turnRight, and turnAround().
Before we can take this step (pun intended) however, we will need to solve a problem. The takeAStep() function works because it is continuously called from within loop. put another way, we are calling and returning from the takeAStep() function until the step is completed. This causes a problem if we want to add additional functionality to our robot. In this instance functions turnLeft, turnRight, and turnAround – and that is the problem. If for instance, if we were to follow takeAStep with turnRight, the program would run the line following code once, then run the turnRight code once, resulting in a really confused robot. Before we solve the problem, lets take a closer look at what is happening. If we were to tunnel down into the C++ code which implements the loop method from within the Arduino scripting language, we would find this while statement.
while(1) { loop(); }
Because the while condition is always true (1), the statements within the while block, in this case loop(), will be repeatedly called until the Arduino is reset. And here in lies the solution to our problem. To keep our takeAStep function from returning, we can “encapsulate” it within its’ own loop. However, in place of a condition which is always true, we will add a condition that will allow our function to return when the robot has completed a single step. Lets take a look at the solution.
This lab is designed to guide you through developing the turning algorithm that will allow your robot to navigate the maze. You will be starting with using only the four IR sensors and a finite state machine algorithm before moving onto a more complex method with one of the following (various types of encoders, gyroscope, accelerometer, color sensors, etc). At the end, you will be choosing the most efficient method that will consistently execute the turns to use in Labs 5, 6, and 7.
Your robot can now follow a line and stop at an intersection (takeAStep) by implementing a simple algorithm which controls the motors based on inputs from the sensors. In this lab, we are focusing on developing the code to cross intersections (hop) and turn the robot to face a desired direction (turnLeft, turnRight, turnAround). When this lab is completed, you will be able to control your robot for all situations it can encounter in the maze and will be ready to develop the control algorithm for navigating your path through the maze in Labs 5, 6 and 7. The critical issue for the turning algorithm is keeping track of the robot’s position with the sensor data that is available. This requires continuous readings from the sensor(s) and implementation of Finite State Machines (FSMs). Along the way we will write four (4) new functions:
- takeAStep()
- turnLeft()
- turnRight()
- turnAround()
Mehtods for Turning
While there are many methods for executing a turn at an intersection, we will be focusing one which requires developing a finite state machine that will accurately identify the position of the robot. You will have a chance to implement other turning methods as part of the design challenge.
FSM
The core concept behind the finite state machine solution is to translate the physical position of the robot when it is over the intersection to specific states that it will go through as it executes the turn. Some things to consider for this is what the initial position should be when the turn is executed. We can start at the moment the robot detects the intersection or after it has moved past the intersection. The initial position will determine the way the robot needs to turn in order to be aligned with the line after the turn.
Initializing the Motors
For our FSM solution to work the turn must start when the robot’s wheels are over the intersection.
Our first attempt at this solution will assume that the turn starts when the robot first detects the intersection. This means that all four IR sensors should be detecting the black line. If we are trying to handle a left turn, it should run the left motor at the minimum speed and the right motor at full speed. That way the robot will arc towards the left and should align with the line after the turn. There will be several transitions that the IR sensors should detect as the robot takes this path and we can associate each with a different state for the finite state machine.
The following algorithm assumes a line following and not an edge following robot with four IR sensors. If you are using the 3DoT IR Shield (Figure 1) the outer sensors use analog inputs and may be used with the AnalogRead() function to more accurately detect when a transition has occurred.
The first state will be when the two outer sensors detect the black line of the intersection. The second state will be when the outer sensor on the right side leaves the black line and hits the white space. To make things easier to detect. During this time, the left outer sensors should also be transitioning to the white space as well. Eventually, the outer sensors will hit the vertical line and that would indicate that the turn is complete.
Apply what we discussed in lab to develop this finite state machine and verify that it works as intended. Please note any changes or modifications that you needed to make.
Prelab 6 – An aMazing Programming Problem
Note: This pre-lab should be completed before starting the first laboratory assignment for this course.
Table of Contents
The Programming Problem
The problem we will be trying to solve for the semester was originally taken from a puzzle book. Here is the problem as defined by the puzzle book.
“In the forest, you will find beehives and more importantly honeycombs. Along the path are bees. The number of bees at any given location is indicated by a number. There are a few ways your bear can travel to the forest. Your aim is to teach your bear how to make his way to the forest while encountering as few bees as possible.”
Take a few minutes and see if you can solve the puzzle. While the problem may seem trivial, we will be using this as an example to show how various programming methods can be used to develop a solution that can be implemented with the assembly language that you learned in lecture. Hopefully, later on in your engineering careers, this sequence of labs will help you realize how there are many ways to resolve a problem and that creating a program may simplify the solution.
Draw a Flowchart
Now let’s see if you can translate your path through the maze into a flowchart. We will need to break it down into the individual actions that the bear can take and make sure that it can be executed by the program. This leads to the following assumptions.
- Assume the hungry Bear is initially facing north with his knapsack. In the knapsack is a blank notepad with a pencil and eraser. (This is our explanation for how the bear keeps track of various values) The length of each step is exactly one square.
- There are certain things that the bear can do or check in the attempt to exit the maze. The bear could take a step into the next room, check to see what type of room is encountered, or decide on which way to go next.
The entire list of instructions that the bear can take are listed below. Compare it with your own list of instructions that you thought to see how close you were.
Actuators and corresponding unconditional instructions
- Take a step
- Turn left
- Turn right
- Turn around
- Count and record the number of bees in your notepad
Sensors and corresponding conditional instructions
- Did you hit a wall?
- Can your left paw touch a wall?
- Can your right paw touch a wall?
- Are you in the forest?
- Do you see any bees?
- Are you thinking of a number {not equal, less than, greater than, or equal} to 0?
- Is the number on page N of the notepad {not equal, less than, greater than, or equal} to some constant?
Notepad operations
The bear can remember 8-bit unsigned and 1-bit (binary) numbers. The bear records a number in his notepad. He can only save one number per page. You may assign a descriptive name to a page (ex. bees), simply use the page number (page1), or think of it as a variable (X). In the following example X = 0.
Pseudocode | C++ Equivalent Instructions |
---|---|
1. Erase page X. | page0 = 0; |
2. Increment the number on a page. | page0++; |
Nodes
- Start
- Stop
Tips and Tricks
- You may not need all the instructions provided.
- Although not required, you can use subroutines.
Take a few minutes to see if you can sketch-out your flowchart. If you don’t know where to start; don’t worry, in the next few sections I will step you through how to write your own flowchart.
The path through the maze can be modeled as follows. Figure 2 provides an overview of the process we will be implementing in our labs. Each block can be considered a collection of the various actions described above and will be expanded on in future labs to describe exactly what our assembly program will be doing. For example, this pre-lab will focus on defining the “Which Way” block, which determines the direction the bear should face while going through the maze.
Creating the Which Way Flowchart
First, we need to clarify what the WhichWay block will be doing. From Figure 2, we know that the bear has just entered a room in the maze and now needs to determine which direction to go. The bear will be taking another step after the Which Way block, so we only need to make sure that the bear is in the correct orientation. There are two ways the bear can decide which way to turn when entering a room. You can count how many rooms the bear has passed or identify what type of room the bear is in. We will be doing the latter for our lab. Based on this information, these are the only instructions needed for this flowchart.
- Turn left
- Turn right
- Turn around
- Did you hit a wall?
- Can your left paw touch a wall?
- Can your right paw touch a wall?
- Increment the number on a page
- Is the number on page N of the notepad {not equal, less than, greater than, or equal} to some constant?
With that in mind, we need to define a way to identify the rooms the bear enters.
Square Naming Convention
Here is a standardized naming convention to help you define the decision points in any maze. In order to provide a design example, the following maze identifies the squares (i.e., intersections) where the bear needs to make a decision for the shortest path solution.
Squares are numbered by concatenating the binary values (yes = 1, no = 0) for the answers to the following three questions (sensor inputs).
Can your left paw touch a wall? – Did you hit a wall? – Can your right paw touch a wall?
The answers to these three questions provide all the information that our bear can know about any given square. Let’s look at a few examples to see how this works. After taking the first step the bear can touch a wall with his left paw (1), has not hit a wall (0), and cannot touch a wall with its right paw (0). For our convention, this would correspond to input condition 4 = 1002. As seen in the illustration, those types of squares are labeled number 4. Assuming the bear turns right; after taking another step the bear finds himself in a hallway where his left and right paws touch a wall and he does not hit a wall. This corresponds to square 5 (1012). Although you could write a 5 in this square, for the sake of brevity, the square is left blank (your bear walks down a lot of hallways). Notice that the numbers are based on the direction the bear is facing and not a universal reference point, like facing north. This corresponds to the fact that within the maze our bear has no idea where north, or any direction for that matter, is (our bear forgot his compass). So, let’s continue to the next intersection. Here the bear’s left paw cannot touch a wall (0), he does not hit a wall (0), and his right paw can touch a wall (1). We therefore would write a 1 (0012) in this square. Continuing in this fashion all intersections are identified for our minimum solution.
Shortest Path Solution
Using the naming convention and the shortest path through the maze presented in the last section, let’s design a solution for the shortest path.
Build a Truth Table
Here are all the possible squares our bear could encounter and a short description of the situation he is facing.
For your minimum solution your bear should encounter squares 1, 3, 4, 5, and 6. Once again we did not include in our illustration situations where the bear has no choice (3 = left corner, 6 = right corner, and 5 = hallway).
Draw your Flowchart – Solution for a Fully “Deterministic” Maze
A fully deterministic maze is one where for any given intersection the bear will always (it is predetermined) take the same action. For example, for your puzzle solution, whenever the bear encounters intersection 4 he will always turn right. Fora a non-deterministic maze he may turn right one time and turn left another. If you look at our shortest solution to the maze you will discover that it is fully deterministic, and so it lends itself to this simple solution.
It is always a good idea to check your answer (or the given one) to see if it actually teaches the bear how to count bees and find the shortest path out of the maze. Once you have your flowchart, implementation in the C programming language or Assembly is fairly straightforward.
Pre-Lab Assignment
In subsequent labs, we will be working with the same bear in the same maze; however you will all be mapping out and trying to teach your bear how to follow a different path. To help everyone plot a unique path, you will need to locate your target square.
Please use the maze included at the start of this lab and “theMaze.bmp” that is linked in the Page 2 section under Deliverable for Prelab 1.
Find Your Target Square
Write down the last four digits of your student ID as two 2-digit decimal numbers. These digits will provide the coordinates (row and column) of your target square. For example, if the last four digits of your student ID were 7386, your two 2-digit numbers would be 73 and 86. Divide by 20 using long division on each number and write the remainder down. Those remainders are now your row and column numbers. In our example, 20 dives into 73 three times with a remainder of 13 and into 86 four times with a remainder of 6. Next convert both numbers into a hexadecimal number. For our example, 13 = 0x0D (where the prefix 0x signifies a number in hexadecimal) and 6 = 0x06. Your target square would therefore be in row 0x0D and column 0x06.
How to Find Your Path
Find a path through the maze such that:
- The bear goes through the target square.
- The bear must get lost at least once. Specifically, he must at some point turn-around. This is typically, but does not need to be, at a dead end.
- There are any number of paths that can take your bear through the target square, get lost, and into the forest, you now want to find the one that results in the numbers of bees encountered being closest to but not exceeding 15 (inclusive).
- Finally, the maze must be non-deterministic. This means that at some intersection along the path the bear will need to take a different action. For example, the first time he encounters a T-intersection he turns left and the second time he turns right. The good news is that, if your path meets the first three criteria, the odds are extremely high that it will be non-deterministic.
Let’s look at how you can develop a flowchart for your unique path.
Design Methodology for a Non-deterministic Maze
As previously mentioned, most maze solutions are non-deterministic. The phrase “not fully deterministic” means, while one set of input conditions in one part of the maze will determine one action (go straight), in another part of the maze the exact same conditions will require a different action (turn right). By looking at your truth-table you can recognize a “non-deterministic” path as having two or more 1’s in the same row. A quick inspection of my truth table reveals that, for the shortest path solution (Figure 4), the bear follows a fully deterministic path. Specifically, for any given intersection the bear will always take the same action. For example, if the bear’s left paw is touching a wall (1), he does not hit a wall (0), and his right right paw is not touching a wall (0), then the bear will always turn right. Following is one path example that illustrates how to solve a non-deterministic maze.
Let’s begin by looking at the sequential actions that must be taken as we encounter each intersection.
The good news is that with the exception of square number 1 all other actions are deterministic. The bad news is that only when we encounter room 1 after the second time do we start turning left. To solve this more difficult problem, we will create a binary tree that allows us to resolve all 8 squares, allowing us to then take any action needed. This binary tree can now be easily translated into C++ or Assembly.
Step-by-Step Instructions
Here are step-by-step instructions for solving your maze.
Begin by making a copy (electronic or paper) of the maze and drawing your bear’s path through the maze. When you are happy with your new path, follow the methodology previously discussed to build your truth table. Verify that your path meets the design criteria (passes through the target square while encountering the minimum number of bees and getting lost once). Remember, your target square may not be along the original solution path.
It is now time to teach your bear how to navigate the new path by writing a flow chart. To accomplish your goal you will need to apply everything you have learned so far plus add a few Notepad operations. The notepad pages (i.e., variables) are used to determine which path your bear should take when he enters an intersection in which more than one action is possible. For example, the first time he enters intersection 1 you may want the bear to go straight, while the second time he encounters intersection 1 you want him to turn left. To resolve this conflict you would record in your notepad how many times intersections 1 had been encountered and then check your notepad before taking any action.
In addition to previously stated conditions, your solution must also meet the following negative criteria.
- Your solution may not use a variable (notepad) to simply count how many steps the bear has taken in order to make a decision.
- Your solution should use a variable(s) and not the number of bees encountered to help it make a decision.
Deliverable for Pre-Lab 6
Turn in the following material on the following pages (i.e., no more, no less). All work must be typed or neatly done in ink.
All labs should represent your own work – DO NOT COPY.
Title Page (Page 0)
The title page (Page 0) includes the lab number, your name, today’s date, and the day your lab meets.
Page 1
At the top of the page provide the last four digits of your student ID and describe how you calculated your target square. Include in your discussion how the resulting path met the design requirements defined in the pre-lab. For example how many paths did you consider before choosing your final path – how close did you come to 15.
Page 2
Next, using your favorite illustration (Visio, Illustrator, or Photoshop) program or the drawing tools included with your favorite Office program (PowerPoint, Excel, and Word) mark your target square with an X and illustrate your bear’s path through the maze. Also include on this page a table of “Sensor input combinations and actions” similar to Table 2. If you do not have access to any of those programs, there is a free online website called draw.io that works just fine.
Many drawing programs allow you to import a bitmap file, in this case the maze. You can find a bitmap and vector formatted picture of the Maze here. Once imported, draw your path, typically using the line tool. Next, number your intersections (but not corners or hallways) as illustrated in Figure 5 “Nondeterministic path example.”
Page 3
Again using your favorite drawing program, draw the flowchart for programming problem.
Your flowchart should resemble the one included with the lab and only use the provided instructions. Artwork of the sample flowchart is included here.
Checklist
- Your pre-lab report includes a title page.
- Pages are in the order specified (see Deliverable)
- You do not have any extra pages
- You describe how you arrived at your path
- Maze is not copied from another student (zero points)
- Path is computer drawn.
- Maze Path meets specified requirements
- Intersections are not drawn by hand and appear as shown in the example
- Intersections are numbered
- Intersections are numbered correctly
- Truth table
- Truth table is on the same page as the maze
- Truth table is typed
- Truth table matches the maze
- Flowchart
- Flowchart matches your truth table
- Flowchart is correct
C++ Robot Lab 6 – Utilizing Arrays & Structures in C++
Table of Contents
Introduction
In the previous labs, you programmed your robot to operate in the real-world. In labs 6 and 7, you will develop the software to keep track of its position in a virtual world. Specifically, you will write functions to teach your robot how to enter and study rooms within a virtual representation of the maze. To accomplish your task, this lab includes a digital description of the maze.
What is New
New terminology and concepts are listed below in black. If you have any questions on this information,
refer back to the lecture on C++ Arrays.
C++ Data Types / Declarations / Definitions
PROGMEM // Variable or value saved to flash program memory
type arrayName[arraySize]; // Array declaration type
type *pointerName; // Pointer declaration
struct name_t{ // Structure Prototype
type var1;
type var2;
type varn; // etc
}
name_t var; // Declare var of data type name_t
Road Map – A Fork in the Road
Figure 1 shows the top-level structure of Labs 6 and 7. We will build this structure and all but one function in Lab 6. We will defer writing the whichWay function to Lab 7.
In writing each block it is important to remember both the point-of-view (pov) and world reality. As shown in the tables to the right of each block, the code may be written from the robot’s pov (1st person) or the maze (3rd person). The reality may be real or virtual.
Labs 1 to 5 developed the code for takeAStep and writing the functions to teach the robot how to turn. All functions controlled the real robot navigating in the real world. Subsequent labs will be operating in the real and/or virtual world from the perspective of the maze or robot. The enterRoom function’s viewpoint is of the robot and its location in the maze. This function mirror’s in the virtual world the real world’s takeAStep function. In contrast, the countBees function is written from the perspective of a virtual robot and is therefore limited to a room. Like takeAStep, the inForest controls the real robot operating in the real world.
The shortest path version of whichWay is shown in Figure 2. The whichWay function is unique in that, while the pov is the robot‘s, the block will operate in both the real and virtual worlds.
Build the Framework
Time to Clean Up
Before we start building our new virtual functions, lets clear some space by moving our functions that operate in the real world to a new file named RealWorld.ino. Click on the drop down arrow on the right hand side of the tab bar, and select New Tab as indicated by the blue rectangle in the figure below. Type out the name of the file (RealWorld) and click the OK button.
Move all your real world robot functions takeAStep, turnLeft, turnRight, turnAround, sensorRead, and walk, to this new file.
The Maze.h Include File
While in the previous labs, your robot rolled around in a real maze, most of our new functions operate on a digital representation of the real maze.
Download a digital representation of the maze here. Place the maze.h file in your project folder and help the preprocessor find it with an include directive, located with the other #include directives at beginning of your program.
#include "3DoTConfig.h"
#include "maze.h"
The quotes (“”) tell the preprocessor to look in the project folder.
In this and future sections, I will be introducing C++ structures. For those of you new to this topic, Appendix A provides an introduction to C++ data structures.
Now, let’s take a closer look at the maze.h file.
The file begins by defining a number of constants. These constants allow us to quickly adapt the robot to different maze types. Specifically, the constants allow us to define the number of rows and columns a maze contains, plus at what point the robot enters the maze.
The file now provides a digital encoding of the real-world maze. Here is the first line of this digital encoding.
const uint8_t theMaze[] PROGMEM =
Here is what it says in “hopefully” plain English.
Define a new array named theMaze[]. Once defined, the array may not be modified (const). The array contains 8-bit unsigned integers (uint8_t). So far so good; but what does PROGMEM mean? The keyword PROGMEM simply says, place this array into Flash into program memory. Which leads naturally to two questions.
Why do we need to explicitly teach the compiler to place the array into Flash program memory?
Most compilers, including ours, were designed for computers based on the Princeton memory model, unlike ours which is based on the Harvard memory model. Put into plain English, most computers like the one you are probably working on right now, only have one type of memory. This is dynamic memory and holds both program and data. In contrast, many micro-controllers, like ours, use Flash memory to hold the program and SRAM to hold the data. As a result, compilers natively, do not know about Flash Program memory and need a special library to help them learn it. For our processor, it is the pgmspace.h library contained in the avr folder. The good news is that PROGMEM; part of the pgmspace.h library is included in all versions of the Arduino after 1.0 (2011), so you will not need to include the library at the top of your sketch.
Why do we want to save this array to Flash program memory?
While the Atmega32U4 contains 32K bytes of Flash program memory, it only contains 2.5 K bytes of SRAM data memory. SRAM data memory needs to hold all of our global variables including variables qualified by the keyword static and const. In addition, SRAM contains the stack used to save all local variables (when the subroutines are called), return addresses, and other data. In other words, SRAM is a valuable and limited resource and should not be used to hold a large array of constants. In contrast, our 32 K bytes of Flash program memory is a perfect place to save this type of data.
Digital Encoding The Maze
The numbers across the top and right side of the encoded maze (Figure 4) correspond to the columns and rows of the maze. You will notice that all of these values are in provided in both hexadecimal and decimal notation.
Each entry in the table defines the room at that row and column address. The definition of the room is encoded in the least significant nibble (4-bits) of each 8-bit entry. The most significant nibble defines the number of bees in the room (0 to 15).
To encode a room, each wall is assigned a bit (Table 1) based on its location along with the points of a compass (Figure 6). Figure 4 shows each room and its resulting bit encoding. For example, the room at coordinates 0,0 has a value of 0x0A = 0b1010. From Table 1, you see that this corresponds to a room with two walls facing North and West.
Direction | North | East | West | South |
Bit | 3 | 2 | 1 | 0 |
Table 1. Wall and Corresponding Bit
Add the following definitions to your 3DoTConfig.h file, where the constant value assigned to a compass point (North, East, West, and South), is the bit position of that wall in a room as defined in Table 1.
#define NORTH 3 // wall bits used to encode a room, and direction bits #define EAST 2 #define WEST 1 #define SOUTH 0
Define and Instantiate two new Data Structures
To navigate a maze the robot will first need a digital description of its location in the maze.
struct coord_t { uint8_t row = 0x0B+1; // locate robot at the entrance to the maze uint8_t col = 0x0F; } coord;
This is the first data structure introduced, so let’s take a closer look. Although a simple two-dimensional array would suffice to define the row and column that the robot currently occupies in the maze, a data structure allows us to do the same thing in a more descriptive fashion. Our structure is named coord_t. The “_t” at the end of the name, is simply a programming convention and means that this is a data type. All instances of our new data structure will have a row and column property. The first instance, of our new data type, is named coord and is initialized at the entrance to my maze. Accordingly, you will want to initialize coord to the entrance to your maze. In this case, the entrance is located in the last column and at the bottom, and just outside, of the maze. Notice that you define an instance of a data structure in an analogous way you define a built-in data type like an int or float.
Next, to add clarity to the program, group these associated variables into a single structure named myRobot_t. This data structure defines the room in which our robot is located (room), the direction he/she is facing (dir), and provides a notepad in which the number of bees encountered up to this point can be recorded.
struct robot_t { uint8_t room = 0x00; uint8_t dir = NORTH; // NORTH,EAST,WEST,SOUTH uint8_t notepad = 0x00; } robot;
The first instance, of our new robot_t data type, is defined with the data structure and is named robot and has my robot facing North. Again, initialize your robot to face in the correct direction.
Place both data structures (coord_t and robot_t) in the 3DoTConfig.h header file along with the points of the compass (last section).
The best method for organizing associated data and functions is in a C++ robot class. This Object Oriented (OOPs) implementation is left as an exercise for the student.
Function enterRoom
Take a Step in the Real and Virtual World
As mentioned in the introduction, your robot currently enters a room within a “real world” maze by taking a step – implemented by the takeAStep() function. This lab begins after this first “real world” step by updating our location in our “virtual world” – implemented by the enterRoom() function. Put another way, until we return from enterRoom, we do not know where the robot is located within the virtual maze.
Here we can see a visual representation of the problem. In the example pictured above, the function takeaStep() is called from the main loop one time. This causes the robot (bear), which was initially in row 0xA0, to take one physical step north and ending up in row 0x90. Without the enterRoom(), the robot has no way of updating its position in the virtual maze and for this reason, it never changes. This means that the physical position will be row 0x90, while the virtual position will remain at 0xA0, leading to a mismatch. In the next section, this problem is solved by writing a new function named enterRoom() which mirrors real-world steps in our virtual world.
enterRoom Template
Complete building the framework by calling enterRoom.
void loop() {
// reality point of view
takeAStep(); // real world robot
robot.room = enterRoom(robot.dir); // virtual maze
}
The objective of the enterRoom function is to mimic in our virtual world the robot taking a step (takeAStep()) in the real world. We are passing the enterRoom function the robot direction dir property as an argument. Add the enterRoom function and use the global instance of the coord_t data type, named coord, to keep track of the location of the robot as it navigates the maze.
/* Virtual World */ uint8_t enterRoom(uint8_t dir) { // pov = maze }
The enterRoom function can be logically separated into three blocks.
Block 1: Update Location
Increment or decrement the row or col property of coord based on the direction the robot is facing (robot.dir) after he/she takes a step (takeAStep) in the real world. This block of code may be implemented in many ways including a series of if-else conditional statements, or conditional operators “?”, a switch-case statement, or as an array. Your solution should look at the direction the robot is facing (dir) and based on this value (NORTH,EAST,WEST,SOUTH) either increment (++) or decrement (–) the robot’s coordinates (coord.row, coord.col) within the maze. For example, if the real-world robot was facing NORTH when he/she took a step, then you would want to decrement the row coordinate of the virtual robot.
Block 2: Out-of-Bounds Check
Once the coordinates of the robot have been updated, verify that the robot is still located within the maze. The dimensions of the maze are defined by constants number_of_rows and number_of_cols. Be careful when checking for out-of-bounds conditions – remember C++ array indexing starts at zero. To simplify your code, the action to be taken if the robot is outside of the maze or enters the forest is the same, specifically, set room equal to a unique number.
room = forest; // robot is in the forest or outside maze boundaries
Where the forest is defined as an 8-bit unsigned integer constant. For future reference group this constant with the compass direction definitions and our two new data structures robot_t and coord_t in the 3DoTConfig.h file.
const uint8_t forest = 0x80;
Block 3: Update Room
If the robot is located within the maze then using the robot’s new position, lookup the room the robot has entered. The maze is located in Flash program memory so you will need to use the pgm_read_byte_near() function located in the pgmspace.h library.
robot.room = pgm_read_byte_near(theMaze + index);
The location of the room within the matrix is a function of the base address (theMaze) plus an index. The index is an unsigned 16-bit integer (uint16_t) and is a function of the row (coord.row), column (coord.col), and the number of columns (number_of_cols). For example, say the robot is heading EAST in a room at map coordinates 0x08, 0x00 (row, column) and then takes a step. The robot is now in the room located at map coordinates 0x08, 0x01. Assuming our maze has 16 columns the room would have an index address of 8×16 + 1 = 129 relative to the base address of the maze.
Test enterRoom
In Lab 5 you could visually see if your takeAStep() function was working in the real world. Unfortunately, you can not tell by simply looking if your enterRoom() function is working in the virtual world. To solve this problem, I have created a lot of handy functions for entering and displaying data. As provided in a “test bench” application, these functions will work together to allow you to watch your virtual robot navigate the maze through Arduino’s IDE Serial Monitor. Quoting Wikipedia…
In the context of software or firmware or hardware engineering, a test bench is an environment in which the product under development is tested with the aid of software and hardware tools. The software may need to be modified slightly in some cases to work with the test bench but careful coding can ensure that the changes can be undone easily and without introducing bugs.
Downoad Testbench.ino here. This is a standalone application and will allow you to test your new enterRoom() function along with two to-be-written functions. This testbench.ino program is pretty much required to get your program debugged.
Open the Testbench.ino program and copy-paste your enterRoom() function from Lab6. Initialize the provided robot_t and coord_t data structures so your robot is at the entrance of the maze. Run the test bench, answering the prompts to move your robot around the maze. At some point navigate your robot out of the maze to verify he/she is in the forest.
When working with TestBench functions and the Serial Monitor always set the line ending to Newline.
Function inForest
While the enterRoom function worked only with the virtual robot, the inForest function works in both worlds. It begins in the virtual world by checking the robot’s notepad to see if he/she has entered the forest. If the answer is yes, then the robot places the motor driver into a low power state and goes to sleep. Add the following conditional statement after enterRoom in the main loop.
void loop() { // reality point of view // takeAStep(); // real world robot robot.room = enterRoom(robot.dir); // virtual maze if (robot.room == forest) { // virtual robot inForest(robot.notepad); // real world robot } }
For my implementation, I simply check to see if the room is equal to the forest. Add the inForest() function after enterRoom() and write the code implementing the comments as explained in the following sections.
/* Real World */ void inForest(){ // pov = robot // Step 1. spin in place n times, where n is the number of bees encountered in the maze. // Step 2. brake and place motors into sleep mode // Step 3. go to sleep http://www.thearduinomakerman.info/blog/2018/1/24/guide-to-arduino-sleep-mode }
Block 1: Dance (Design Challenge #1)
Once you enter the forest isolate the number of bees encountered during your robots journey through the maze.
uint8_t n = (robot.room & ~forest) >> 4;
This C++ statement first clears the most significant bit and then shifts the number of bees in the most significant nibble to the least significant nibble. The resulting total number of bees is then saved to variable n. Now add the code to teach your robot how to spin n times.
Block 2: Motor Driver to Standby
Review C++ Robot Lab 1, Section 7 “Controlling the Motors” for help on placing the motor driver into standby mode.
Block 3: Sleep (Design Challenge #2)
There are a number of ways to put your robot to sleep. The simplest is the while(true); statement. Because a while loop will continue until the test condition is false, this while loop will continue forever, or at least until the reset button is pressed, switched off, or battery runs out of power (technically an under voltage lock-out condition occurs).
Your second design challenge is to put the MCU to sleep. Read this article on “Putting your Arduino to Sleep” or any of the many other articles and videos on this subject.
Test inForest
Run Testbench.ino, answering the prompts to move your robot around the maze. At some point navigate your robot out of the maze to verify he/she is in the forest. You will need to test the inForest function in the real world.
Function countBees
As the robot navigates the maze he/she will encounter bees (or other objects to be recorded). Fortunately, our robot carries a notebook where he/she can record the number of bees found during his/her journey through the maze.
Each entry in maze.h encodes both the number of bees and the room number. The most significant nibble of the byte holds the number of bees, while the lease significant nibble equals the room number. The countBees function removes the bees from the byte and adds them to the number of bees already recorded in the notepad.
Add the countBees function call following the conditional statement in the main loop.
void loop() { // reality point of view // takeAStep(); // real world robot robot.room = enterRoom(robot.dir); // virtual maze if (robot.room == forest) { // virtual robot inForest(robot.notepad); // real world robot } robot.notepad = countBees(robot.notepad, robot.room); // virtual robot
Add the countBees() function after the enterRoom() and before the inForest() function.
uint8_t countBees(uint8_t notepad, uint8_t bees) { // pov = robot shift bees into the least significant nibble and add to notepad return notepad }
For your solution, you may want to use the C++ Boolean bitwise right shift (&, |, <<, >>, ~, ^) and assignment (=, +=, &=, etc.) operators. If you need help, please review my C++ introductory lecture.
Test countBees
Open the Testbench.ino program and copy-paste your countBees function from Lab 6. In the maze.h file included with the Testbench application, I have included some bees near the entrance defined in the default starting location. Launch the test bench and navigate your robot around the default entrance (0x0B, 0x0F) recording how many bees he/she has discovered.
In this example, I entered the maze at the default entrance immediately encountering 2 bees. I then turned west and encountered one more bee bringing the total number of bees discovered to 3.
Design Challenge(s)
Your two design challenges are located in the inForest section of the lab.
Lab 6 Deliverable(s)All labs should represent your own work – DO NOT COPY. Submit all of the files in your sketch folder. Make sure that the code compiles without any errors. Be ready to demonstrate your enterRoom(), inForest(), and CountBees() functions on the testbench. |
Checklist
Remember before you turn in your lab…
- Your enterRoom function implements the three blocks of code.
- Your countBees function implements the two defined tasks.
- Your inForest function implements the three blocks of code.
Appendix A: Data Structures
Data Structures in C++ provide a greater level of organization for complex systems. Before going into greater details; arrays will be reviewed as they give a good insight on how structures work. for example: type array[size]; C++ arrays are a fixed length of a singular data type. This allows us to collect groups of numbers for data for analysis. The issue comes when collections of data are not conducive to arrays or if there is a mixture of data types. Starting from basic C++ foundation, the immediate idea is to make sets of arrays and organize the groups of information by index. That is the core premise of structures! Structures also take us one step closer to Object Oriented Programming (OOP).
Structure… Structure:
From “What’s New” Section:
struct name_t{ // structure prototype
type var_1;
type var_2;
type var_n; // etc
}
You can now declare variables of this data type, just as if they were primitive data types.
name_t var; // Defines a variable of type name_t
Structures enable the user to define sets of data and with a variety of data types based on their needs.
For an example:
struct student_t{ // structure prototype
string name; // C style string (Char array)
uint8_t age; // integer for age
uint8_t classes[7]; // array of size 7 for their class list.
float GPA; // float with decimals.
}
student_t student; // Defines variable student of type student_t
Looking at this example, structures establish a blueprint for how the information you want to store is grouped. Similar to how you can declare more than one instance of any type, we can do the same with this structures now!
For example:
int x;
int y;
student_t student1;
student_t student2;
etc.
Manipulating the information in the struct requires a new notation, the dot (.) operator. This enables us to explicitly change a member within the structure.
It looks like:
student1.age = 21; //or
student1.age = x // assuming x is a uint8_t.
Data type rules still hold true. Since age within the structure was defined as an uint8_t, the compiler will check to make sure a uint8_t is placed in that variable. Finally note how we can choose to say the age of Student2 is different than Student1. This flexibility lays the foundation for how we want to define our structure in this Lab.
C++ Robot Prelab 7 — Navigation Building Blocks
Table of Contents
Macros versus Subroutines
All compilers take two passes through your code in order to generate the machine instructions uploaded to the MCU. On the first pass macros (#define) are expanded to a list of C++ instructions, which are then compiled on the second pass. In contrast subroutines are compiled on the second pass. So when should you use a macro or a subroutine. Macros run faster than subroutines, because they are in-line with the rest of your code, while a subroutine must be called and subsequently return. Subroutines use less memory than macros because the code is not duplicated each time it is used. In some embedded applications, the microcontroller has limited Flash program memory and timing is not critical, so subroutines make sense. This is also the case if the code to implement a function is long and called many times. In other applications, timing is critical and macros make sense. This is also the case if the code to implement the function is short (one line) and it is only needed a few times. So the answer to which to use at any given time is application specific.
In this prelab, we will look at how to program navigational building blocks using macros and subroutines.
Reading
Lookup Tables
After reading Section 4.3 Digital Encoding The Maze, and studying the look_right[] sample, complete array declarations look_left[]and look_back[].
/* Look around
* look-up tables use direction the robot is facing as index
* index 0 1 2 3
* robot is looking = SOUTH WEST EAST NORTH
*/
const uint8_t look_right[] = {WEST,NORTH,SOUTH,EAST};
const uint8_t look_left[] = {____,_____,_____,____};
const uint8_t look_back[] = {____,_____,_____,____};
Using one of the look-up tables and the direction the robot is looking, (robot.dir), write a C++ statement that will tell us in which direction the robot will be looking if its head turns to the left or right.(EAST,SOUTH,NORTH,WEST).
robot looks right = _______________________;
robot looks left = _______________________;
Test if Bit is Set
After reading Lecture 3 C++ Type of Data and Playing with Bits, answer the following three questions.
All macro definitions evaluate to a value of zero (false) if the bit within the test byte is clear; otherwise evaluate to true. Where true is defined as any number that is not zero.
- What macro is defined as (1 << (bit))?
- What macro (#define) allows you to test if a special function register (sfr) bit is set?
- Write a macro named _TestBit to test if bit in byte_value is set.
#define _TestBit(byte_value, bit) _________________
Test Room Bits
Apply what you learned to write the following macros.
#define _hitWall(room, dir) ______________________________ #define _rightSensor(room, dir) __________________________ #define _leftSensor(room, dir) __________________________
Each macro evaluates to a value of zero (false) if there is no wall in the direction the robot is looking; otherwise evaluates to true. Where true is defined as any number that is not zero.
Hint: While _hitWall directly follows from your answer to question 3, macros _rightSensor and _leftSensor will use arrays look_right and look_left respectively to identify the bit to test.
Macros versus Subroutines
Convert your macros in the last section into subroutines. Please, do not simply add the macro.
bool hitWall(room, dir) { return ______________________________; } bool rightSensor(room, dir) { return ______________________________; } bool leftSensor(room, dir) { return ______________________________; }
Each subroutine returns to Boolean value of 0 (false) if there is no wall in the direction the robot is looking; otherwise returns 1 (true).
Deliverable(s)On page 1 include your name and lab along with the answers to questions in the first two blocks. On page 2, turn in a print-out of your Arduino test script containing the 3 macros and 3 subroutines. All macros should be tested in a new Arduino script and compile without any errors. The prelab should represent your own work – DO NOT COPY. |
C++ Robot Lab 7 — Navigating the Maze
Table of Contents
Introduction
For the first time in Lab 7, our real world robot meets the virtual world. In the early labs the real world robot learned how to takeAStep, turnRight, turnLeft, and turnAround. Lab 6 created a virtual representation of the real world maze that the robot would need to navigate. The first use of this virtual maze was to map a step taken in the real world (takeAStep) to a corresponding step in the virtual world (enterRoom). The lab also had our robot study the room; answering questions concerning the number of bees (countBees) and whether or not it was in the forest (inForest). In this lab our robot will ask more questions about the virtual room in order to take action in the real world (takeAStep, turnRight, turnLeft, and turnAround) – and the circle is complete.
Build the Framework
Open Lab6 in the Arduino IDE. Save As Lab 7.
Create a new tab in the Arduino IDE, by clicking on the down-arrow located on the right-hand side of the tab bar. Name the tab VirtualWorld.
If you have not done so already, add the three (3) look-up table you wrote in the VirtualWorld prelab, here or in 3DoTConfig.h.
/* Look around */ // look-up tables use direction the robot is facing as index // SOUTH WEST EAST NORTH Direction robot is facing const uint8_t look_right[] = {WEST,NORTH,SOUTH,EAST}; const uint8_t look_left[] = {____,_____,_____,____}; const uint8_t look_back[] = {____,_____,_____,____};
In order for the robot to navigate through the maze, decisions will have to be made. These decisions include: continue straight, turn left, turn right and turn around. However, these actions depend on the type of room in which the robot is located. Currently you can keep track of the robot’s position throughout the maze virtually. Functions hitWall, leftSensor, and rightSensor, written in the prelab, now give you the ability to answer questions about this virtual room. If you have not done so already, place these three functions into the VirtualWorld tab.
/* check if the path in front of the robot is clear */ bool hitWall(robot_t robot) { } /* check if the path to the left of the robot is clear */ bool leftSensor(robot_t robot) { } /* check if the path to the right of the robot is clear */ bool rightSensor(robot_t robot) { }
Which Way?
The last piece of the puzzle is translating your prelab 6 whichWay flowchart into a C++ program. The simplest way to help you translate your flowchart into code is by example. The flowchart in Figure 1.0 teaches your robot how to follow the shortest path through the maze.
Add a call to the whichWay function at the end of loop(), followed by the shortest path solution.
void loop() { // reality point of view // takeAStep(); // real world robot robot.room = enterRoom(robot.dir); // virtual maze if (robot.room == forest) { // virtual robot inForest(robot.notepad); // real world robot } robot.notepad = countBees(robot.notepad, robot.room); // virtual robot robot.dir = whichWay(robot.room, robot.dir); // both robot } /* Real and Virtual World */ uint8_t whichWay(uint8_t room, uint8_t dir) { room &= 0x0F; // clear the most significan nibble if (!rightSensor(room, dir)){ dir = look_right[dir]; // update virtual world turnRight(); // turn robot in the real world } else if (hitWall(room, dir)){ dir = look_left[dir]; // update virtual world turnLeft(); // turn robot in the real world } return dir; }
The whichWay function takes as inputs robot properties dir and room. and returns the new direction the robot is facing.
The flowchart conditional blocks (diamonds) are implemented using if-else statements. The conditional expressions query the nature of the room by calling three functions, which answer the following questions:
Function | Question |
rightSensor | Does my right “virtual” sensor detect a wall? |
leftSensor | Does my left “virtual” sensor detect a wall? |
hitWall | Is there a wall in front of me? |
Note: The rightSensor is not required for the robot to follow the shortest path.
Based on the type of room the robot finds itself, action blocks “Turn Right” and “Turn Left” are called. The objective of the first statement in each block is to mimic in the virtual world the robot turning in the real world, which is done in the second line.
Now that all the pieces of the puzzle are in place, your robot should be able to navigate the shortest path through the maze.
Test whichWay
Apply what you learned in lab 6 to debug your code. For example, you can add this function at the end of your whichWay routine to see which direction your virtual robot is facing.
write_dir("I am looking ",dir,10);
Lab 7 Deliverable(s)All labs should represent your own work – DO NOT COPY. Submit all of the files in your sketch folder. Make sure that the code compiles without any errors. |
Checklist
Remember before you turn in your lab…
- Completed shortest path whichWay.
- Your whichWay sketch with comments.
- Demonstration of your robot running the shortest path and hopefully your path through the maze.
Basic Robot Kit Parts
Robot Base Configuration
Electronics
Pro Micro – 3.3V/8MHz | 1 | $19.95 | $19.95 |
SparkFun – Dual Motor Driver TB6612FNG (w/ Headers) | 1 | $5.45 | $5.45 |
$25.40 |
Wires/Hookup
Jumper Wires (6″ M/F) | 1 | $1.95 | $1.95 |
Jumper Wires (6″ M/M) | 1 | $1.95 | $1.95 |
Breadboard – Self-Adhesive (White) | 1 | $4.95 | $4.95 |
$8.85 |
Sensors
SparkFun Line Sensor Breakout – QRE1113 (Analog) | 4 | $2.95 | $11.80 |
(Optional) SparkFun RGB Light Sensor – ISL29125 | 2 | $7.95 | $15.90 |
(Required with RGB Sensors) I2C Multiplexer | 1 | $8.79 | $8.79 |
(Optional) Arduino Analog Grayscale Sensor | 2 | $3.27 | $6.54 |
$11.80 |
Chassis
Circular Robotics Chassis Kit (Three-Layer) | 1 | $19.95 | $19.95 |
Screw – Phillips Head (1″, 4-40, 10 pack) | 1 | $0.46 | $0.46 |
Nut – Metal (4-40, 10 pack) | 1 | $1.50 | $1.50 |
Standoff – Nylon (4-40; 3/8″; 10 pack) | 1 | $2.95 | $2.95 |
Hobby Gearmotor – 200 RPM (Pair) (ECS 316) | 1 | $3.95 | $3.95 |
$28.81 |
Power (Recommended Option)
Lithium Ion Battery – 2Ah | 1 | $12.95 | $12.95 |
SparkFun LiPo Charger/Booster – 5V/1A | 1 | $14.95 | $14.95 |
$27.90 |
Total Cost: $112.75
Power (Cheaper Option)
Battery Holder – 4xAA to Barrel Jack Connector (ECS 316) | 1 | $2.49 | $2.49 |
DC Barrel Jack Adapter – Breadboard Compatible (ECS 316) | 1 | $0.95 | $0.95 |
1500 mAh Alkaline Battery – AA | 4 | $0.50 | $2.00 |
$5.44 |
Total Cost (w/ Cheaper Option): $90.29
Extra Sensors
Before lab 3, teams will need to pre-select and research a sensor allowing the robot to turn left, right, around and spin in place. These will vary in price, difficulty and effectiveness. These are just a few known options:
- IR Reflective Sensor
- IR sensor
- plus Interrupt driven intersection detection with Finite State Machine (FSM) interrupt service routine (ISR)
- Sparkfun I2C modules
- Compass
- Gyro with Matlab option
- IMU
- plus filtering techniques
- Rotary also called shaft encoders
- Conductive – Quadrature
- Conductive – Pulse with Interrupts
- Optical (slit and reflective)
- Magnetic (on-axis and off-axis)
- Capacitive
- Resistive
- plus encoding techniques
- Ultrasonic Sensor for turns and tracking
- Creative Ideas Welcomed