Skip to content

Interfacing the Incremental Encoder from the Gear Motor

GTA Marking

This is an assessed Exercise. When you have completed the Assessed Exercise, you should show your work to a GTA to get marked.

Prerequisite for this Exercise

Before starting this exercise, verify that you have completed the circuit on the robot chassis as illustrated in Building the Robot: Circuit Layout for the Encoders and Motor Exercise and all connections are correct and secure.

Before you start this exercise, you should complete the DC Motor Exercise.

The following video is a quick demonstration of the final outcome from this exercise:

Video demonstrating the expected outcome from this exercise

Introduction and Background

Rotary sensors measure the angular position and/or rotational speed of a rotating body (typically a shaft) using either analog or digital outputs. The term rotary encoder generally describes a digital rotational position sensor.

Rotary encoders fall into two primary categories:

  1. Absolute Position Encoders: Devices in this class output the exact angular position of the rotating shaft immediately upon power-up, without requiring an initial zero reference or homing sequence.
  2. Incremental Position Encoders: Devices in this class report relative changes in angular displacement from a starting reference point. Absolute position can only be determined if the system records a known initial start-up position or detects an explicit index pulse (Z-channel).

For additional background on encoder working principles, refer to the Wikipedia Entry on Rotary Encoders.

Rotary speed can be measured directly using a rotary shaft encoder designed as a tachometer (though dedicated tachometers will not be covered further in this document). Alternatively, rotational speed (\(\omega\)) can be calculated from a position encoder by measuring angular displacement (\(\Delta\theta\)) over a known sampling time interval (\(\Delta t\)):

\[\omega = \frac{\Delta\theta}{\Delta t}\]

Sensing technologies for position encoders vary across designs, with optical and magnetic sensing being the two most common approaches. The incremental encoder integrated into the FIT0450 geared DC motor (included in your Mechatronics kit) is a magnetic quadrature encoder. Quadrature encoders output two channel square waves, designated Channel A and Channel B, which are phase-shifted relative to each other by \(90^\circ\), as illustrated in Fig 2.

Example of the quadrature encoder output.
Example of the quadrature encoder output.

Fig 3 shows the magnetic quadrature encoder integrated into the FIT0450 geared DC motor assembly. This actuator features a \(120:1\) reduction gearbox positioned between the DC motor shaft and the primary output drive. The sensing hardware consists of an \(8\)-pole-pair magnetic disc mounted directly onto the rear armature shaft, paired with two spatially offset Hall-effect switch sensors fixed to the motor casing.

Analogue Sensors vs. Digital Switch Sensors

An analogue sensor produces a continuous, variable output signal (such as a changing voltage level) that typically requires external signal conditioning or filtering before being processed by a microcontroller's ADC.

In contrast, a digital switch sensor incorporates internal signal-conditioning circuitry (e.g., Schmitt triggers or comparators) to directly output clean HIGH (\(5\,\text{V}\)) or LOW (\(0\,\text{V}\)) logic levels, enabling direct connection to digital GPIO pins.

Connections between the encoder and the Arduino and components of the encoder.
Connections between the encoder and the Arduino and components of the encoder.

As the magnetic disc rotates, each transition of a north pole past a Hall-effect switch toggles the output logic state from 0 to 1 (and back to 0 when a south pole passes). This continuous transition produces a digital square wave at the output of each switch sensor. The encoder contains two outputs, Channel A and Channel B, corresponding to the two Hall sensors. The physical sensors are spatially positioned \(90^\circ\) electrically out of phase around the disc's circumference. Consequently, outputs A and B generate square waves offset by \(90^\circ\), as shown in Fig 2. This phase shift is critical for directional decoding: Channel A leads Channel B during clockwise rotation, whereas Channel B leads Channel A during counter-clockwise rotation.

For the FIT0450 motor, the magnetic disc contains \(8\) pole pairs, yielding \(8\) full square-wave pulses per sensor for a single revolution of the motor armature shaft. Because the motor is fitted with a \(120:1\) reduction gearbox, one complete revolution of the main output shaft corresponds to:

\[ 8 \text{ pulses/rev} \times 120 = 960 \text{ pulses per output shaft revolution} \]

Counting both rising and falling edges across both channels quadruples the available measurement resolution (\(960 \times 4 = 3,840 \text{ state transitions/rev}\)). Base pulse-counting accuracy without quadrature edge decoding corresponds to \(960 \text{ PPR}\), equivalent to:

\[ \frac{360^\circ}{960 \text{ pulses}} = 0.375^\circ \text{ per pulse} \]

At higher rotational speeds, encoder outputs generate high-frequency pulse trains. Polling pin states sequentially inside the standard Arduino loop() function is unreliable; if the microcontroller is executing other tasks during a pin transition, pulses will be missed, leading to cumulative position errors. To ensure reliable signal processing, encoder channels should be sampled using hardware interrupts. The microcontroller's interrupt module detects logic state changes on dedicated General Purpose Input/Output (GPIO) pins, temporarily pausing main thread execution to instantly run a specialized function known as an Interrupt Service Routine (ISR).

Reading a quadrature encoder relies on level-change interrupts configured on the microcontroller's digital input pins connected to Channels A and B. Whenever a logic-level transition occurs on either channel, the main execution thread is interrupted and the corresponding Interrupt Service Routine (ISR) is immediately executed. Consequently, processor tasks are temporarily paused to service the encoder event, preventing lost pulses.

In the case of reading the quadrature encoder, we use a level change interrupt on the pins of the microcontroller, attached to the quadrature encoder. Each time there is a level transition in either output A or B, the main code is interrupted and the ISR is run by the microcontroller. This means that whatever the microcontroller is busy doing, the execution is stopped to process the interrupt, and no pulses are lost.

Interrupts can be configured in multiple ways to track encoder outputs. Achieving maximum resolution (\(4\times\) decoding) requires configuring interrupts on both input pins, whereas using a single interrupt on one channel yields half the resolution (\(2\times\) decoding).

The dual-interrupt method, outlined in Table 1, uses a position counter variable alongside two distinct ISRs to track movement. One ISR is attached to Pin A to process transitions on Channel A, while a second ISR is attached to Pin B for Channel B.

Table 1 details the operational logic for both ISR handlers. When an interrupt occurs on either pin, the current state of Channel A is compared against Channel B. Based on these pin states and the logical conditions summarized in Table 1, the position counter is either incremented or decremented. Table 1 also indicates which ISR processes each specific condition row.

ROTATION:OUTPUT A:OUTPUT B:Which ISR?COUNT:
CW0→10A+1
10→1B+1
1→01A+1
01→0B+1
CCW0→11A-1
11→0B-1
1→00A-1
00→1B-1
Control operation of the H-bridge.

The code snippet below provides a framework for configuring digital pins D2 and D3 on the Arduino Uno as interrupt-capable inputs. Using the attachInterrupt() function, these pins are assigned to their respective ISRs, ChannelA() and ChannelB(), which trigger on any state transition (CHANGE).

How to setup The D2 and D3 pins as interrupt capable
#define PINA 2 // A output of the  encoder attached to PIN 2 of the Arduino
#define PINB 3 // B output of the  encoder attached to PIN 3 of the Arduino

volatile long enc_count = 0; // Pulse count from the quadrature encoder

void setup(){
  // Initialize the A and B input pins
  pinMode(PINA, INPUT);
  pinMode(PINB, INPUT);

  // Attach interrupt to PINA. When the pin changes state, the function 
  // channelA() is called.
  attachInterrupt(digitalPinToInterrupt(PINA),channelA, CHANGE);
  // Attach interrupt to PINB. When the pin changes state, the function 
  // channelB() is called.
  attachInterrupt(digitalPinToInterrupt(PINB),channelB, CHANGE);

}

void loop() {
...
}

void ChannelA(){
...
}

void ChannelB(){
...
}

For a detailed technical description of the function, refer to the official Arduino attachInterrupt() Reference Guide.

The CHANGE interrupt mode is passed to attachInterrupt() to ensure an interrupt fires on both rising (\(0 \rightarrow 1\)) and falling (\(1 \rightarrow 0\)) signal edges on Channels A and B, maximizing pulse count resolution.

The volatile Keyword Qualifier

The enc_count global variable is declared with the volatile qualifier. Any variable modified within an ISR and read in the main loop must be qualified as volatile. This directs the compiler to load the variable from RAM rather than caching it in a CPU register, ensuring thread-safe reads during asynchronous interrupt events.

For further information, consult the Arduino volatile Reference Guide.

Arduino Hardware Interrupt Pin Allocation

On the Arduino Uno, hardware interrupts are available strictly on digital pins D2 (Interrupt 0) and D3 (Interrupt 1).

On the Arduino Mega 2560, pins D2, D3, D18, D19, D20, and D21 support external interrupts. Note that pins D20 (SDA) and D21 (SCL) cannot be used for interrupts if \(\text{I}^2\text{C}\) bus communication is active (though \(\text{I}^2\text{C}\) is not utilized by the sensors in this Mechatronics kit).

Before implementing closed-loop DC motor control, converts raw pulse counts into physical engineering units—such as output shaft revolutions or angular position (\(\theta\)). Calculate output shaft revolutions using:

\[ shaft \space revolutions=\frac{pulse \space count}{pulses \space per \space revolution} \]

To compute angular displacement in degrees (\(^\circ\)):

\[ shaft \space angle=360°\times\frac{pulse \space count}{pulses \space per \space revolution} \]

Quadrature Edge Counting & Resolution Scaling

The equations above assume a pulse count based on full square-wave cycles (\(1\times\) resolution). Because the provided dual-ISR framework triggers on every rising and falling edge across both channels (\(4\times\) quadrature state decoding), the total accrued edge count is four times higher.

Reading the Output of the Incremental Encoder

The aim of this exercise is to sample the rotary encoder position using interrupts and continuously output the accumulated position data to the Arduino IDE Serial Monitor.

Before proceeding, verify that all hardware wiring and pin connections comply with the specifications in the Building the Robot: Circuit Layout for the Encoders and Motor Exercise guide.

The following code template below provides the foundation for reading the FIT0450 incremental encoder. This sketch is incomplete; you must apply the technical principles covered in the preceding theory sections to implement the missing logic during the assessed exercise below.

TwoInterruptEncoder.ino Example Code Template
#define ENC_K [Insert the value for the FIT0450 Gearmotor] //Number of edges
//  per revolution of the output shaft
#define PINA 2 // A output of the quadrature encoder attached to PIN 2
#define PINB 3 // A output of the quadrature encoder attached to PIN 3

//These two variables are declared as volatile because they are updated
// within an ISR
volatile long encCount; // Pulse count from the quadrature encoder
volatile float wheelAngle; // Angle of the output shaft of the gear motor,
// attached to the wheel

// initialise the lastDisplay variable
long lastDisplay = 0;
#define delayDisplay 250

void setup() {
  Serial.begin(9600);
  pinMode(PINA, INPUT);

  // Attach interrupt to PINA. When the pin changes state, the function channelA()
  // function is called.
  attachInterrupt(digitalPinToInterrupt(PINA),channelA, CHANGE);

  // Attach interrupt to PINB. When the pin changes state, the function channelB()
  // function is called.
  attachInterrupt(digitalPinToInterrupt(PINB),channelB, CHANGE);

  encCount = 0;
  wheelAngle = 0;
}

void loop() {

  // Update the Serial Monitor every 250ms
  if (millis() >= lastDisplay + delayDisplay) {

    // update lastDisplay to the current millis() time
    lastDisplay = millis();

    // construct the serial monitor messages
    Serial.print("Encoder Pulse Count = ");
    Serial.print(encCount);
    Serial.print("Wheel angle = ");  
    Serial.print(wheelAngle);
    Serial.println("°");
  }
}

void channelA() {
// Use if else statements to increment or decrement the encCount variable
// Use information in the table in the Encoder section of the The Rotary 
// Position Encoder section of the laboratory Documentation.
// Focus on the table rows where Output A is changing
// Use digitalread() to acquire the states of PINA and PINB

  if([insert a logic statement here]) {
    if([insert a logic statement here]){
      encCount++; // Encoder rotating in one direction, e.g. clockwise
    }
    else {
    // Encoder rotating in the opposite direction, e.g. counter clockwise
    encCount--; 
    }
  }
  else{
    if([insert a logic statement here]){
      encCount++; // Encoder rotating in one direction, e.g. clockwise
    }
    else {
    // Encoder rotating in the opposite direction, e.g. counter clockwise
    encCount--; 
    }
  }  

  //compute the angle of rotation of the wheel using the pulse count, encCount
  wheelAngle = ; 

}
void channelB() {
  // Use function channelA() as a template
  // This time focus on the rows of the table where Output B is changing
}

Quadrature Edge Count vs. Pulse Count

This dual-interrupt implementation detects every rising and falling logic edge across both Channel A and Channel B signals (\(4\times\) quadrature edge decoding), rather than counting full square-wave pulses. Consequently, the accumulated count will be four times greater (\(4\times\)) than the raw pulse count.

Reading the Rotary Encoder Exercise

The aim of this exercise is to implement and verify rotary encoder signal acquisition prior to integrating DC motor actuation.

While this exercise is non-assessed, completing it is strongly recommended before attempting subsequent tasks, as it establishes the foundational pulse-counting functionality required for the Assessed Exercise.

Procedure:

  1. Copy and paste the starter code template provided above into a new Arduino sketch.
  2. The ISR for Channel A, ChannelA(), is partially implemented. Complete the function by applying the state-evaluation logic defined in Table 1.
  3. Based on your implementation of ChannelA(), write the corresponding logic for the ChannelB() ISR function.
  4. Modify the code to update and print the encoder values to the Serial Monitor every \(250\,\text{ms}\).
  5. Ensure that the ENC_K constant is set to the total number of digital edges per output revolution, rather than the raw pulse count.
  6. Upload the code to your Arduino board and manually rotate the motor wheel. Verify that the Serial Monitor outputs accurate, real-time positional data and correctly indicates direction changes.

Avoid Using the Delay() Function

For the timing of the serial monitor and other events, you should use the millis() method in the TwoInterruptEncoder.ino template file, and not use the delay() method.

Using blocking functions like delay() is bad practice in embedded software development; it freezes processor execution, delays non-interrupt loop tasks, and causes significant timing jitter in multitasking systems, preventing your code from executing in a deterministic manner.

Assessed Exercise

The aim of this exercise is to measure the angular position of the motor wheel using the quadrature encoder and control the motor actuation to position the wheel accurately across a sequence of target setpoints.

During this exercise, you are required to integrate your implementations from the following preceding tasks:

Procedure:

Write a program that can rotate the wheel connected to the DC motor according to the following cycle:

  1. From an initial starting point, rotate the wheel 45° clockwise
  2. Wait for 1 second
  3. Rotate the wheel 45° anticlockwise (back to the starting position)
  4. Wait for 1 second
  5. Rotate the wheel 90° clockwise
  6. Wait for 1 second
  7. Rotate the wheel 90° anticlockwise
  8. Wait for 2 seconds, then repeat the sequence.
  9. On the serial monitor, you should display:
    • The time in seconds from the start of the demo
      • You should calculate this, not use the serial monitor time stamp
    • The PWM output to the motor
    • The shaft position of the motor in degrees.

Non-Blocking Timing Requirements

Remember: do not use blocking functions such as delay() in your code. Implement non-blocking timing using the millis() function, as demonstrated in the TwoInterruptEncoder.ino template file above.

Exercise Assessment

What we expect to see from your demonstration?

  • Demonstrate that you have fulfilled the requirements of the exercise with your working system.
  • The rotational sequence for the output shaft as described above.
  • An external power supply is used to power the motor circuit, (Vm on the driver board).
  • Demonstration time in seconds, motor PWM value, control logic Boolean values to the driver board, and the motor shaft angle, are clearly labelled and displayed on the serial monitor.
  • The serial monitor should update at a reasonable rate – 2 to 4 times a second.

Now Get Your Work Marked by a GTA

Once you have completed your code and are satisfied with its operation, you should show your work to a GTA for marking.

Extension Exercise: Reading multiple encoders efficiently. (Non-Assessed)

Formative Exercise

This extension exercise is non-assessed and formative. Completing this task is recommended, as the interrupt techniques covered here may be very useful for your group project.

The aim of this exercise is to implement Pin Change Interrupts (PCINT) on micro-controller I/O ports rather than relying exclusively on dedicated external interrupt pins (INT0/INT1).

Note: Pin Change Interrupts are a hardware feature specific to AVR microcontrollers (e.g., ATmega328P). Implementation details and register configurations may vary across different architecture families.

When controlling differential-drive or multi-axis mobile robots, multiple quadrature encoders must be sampled simultaneously. On the Arduino Uno, external hardware interrupts via attachInterrupt() are constrained strictly to digital pins D2 and D3. Upgrading to a larger development board—such as the Arduino Mega 2560—provides additional external interrupt pins, but scaling to higher encoder counts requires a more flexible architectural solution.

A scalable and hardware-efficient approach for acquiring two or more quadrature encoders is utilizing Pin Change Interrupts (PCINT). On AVR microcontrollers, General Purpose Input/Output (GPIO) pins are organized into 8-bit hardware Ports. For instance, on the Arduino Uno (ATmega328P), physical I/O pins map directly to Port B, Port C, and Port D, as illustrated in Fig 4.

Pinout of the Arduino UNO board.
Pinout of the Arduino UNO board.

In this exercise, you will consider Port D pins on digital I/O D0 through D7, as illustrated in Fig 4.

Direct Hardware Register Configuration

This advanced method bypasses standard Arduino framework abstraction functions like attachInterrupt(). Instead, you will directly configure the microcontroller's low-level Control Registers (PCICR, PCMSK2), providing faster execution and lower interrupt overhead.

To implement Pin Change Interrupts (PCINT) on Port D, execute the following configuration steps:

  1. Pin Grouping: Identify the input pins to attach interrupts to, ideally selecting pins on a single 8-bit port to simplify state masking.
  2. Port Enable: Enable the Pin Change Interrupt Control Register (PCICR) for the selected port bank.
  3. Pin Masking: Select specific interrupt pins by updating the corresponding Pin Change Mask Register (PCMSK2).
  4. ISR Implementation: Write the dedicated Interrupt Service Routine vector (e.g., ISR(PCINT2_vect)).
    • Within the ISR, evaluate the port's current state against its previous state to determine which specific pin triggered the transition and increment/decrement position counters accordingly.

For a detailed walkthrough of register flags and hardware vectors, refer to the following article: DroneBot Workshop Interrupts Guide.

The principal advantage of direct register configuration is flexibility: almost any microcontroller pin can serve as an interrupt source, significantly expanding encoder channel capacity.

Consider a dual-encoder setup on an Arduino Uno:

  • Encoder 1 (Left): Channel A \(\rightarrow\) Pin D3, Channel B \(\rightarrow\) Pin D4
  • Encoder 2 (Right): Channel A \(\rightarrow\) Pin D5, Channel B \(\rightarrow\) Pin D6

As shown in Fig 4, pins D3 through D6 all map directly to physical Port D (PORTD and PIND registers), allowing a single ISR vector to service both quadrature encoders simultaneously.

The code snippet below outlines the basic register initialization sequence required to configure Pin Change Interrupts (PCINT) on Port D:

  1. *Enable Port D Interrupt Bank: Activate the Pin Change Interrupt bit for Port D (PCIE2) in the Pin Change Interrupt Control Register (PCICR):

    PCICR |= 0b00000100; // Set bit 2 (PCIE2) to enable PCINT for Port D (pins D0–D7)
    
  2. Clear Mask Register: Reset the Port D Pin Change Mask Register (PCMSK2) to clear any residual bit configurations:

    PCMSK2 = 0; // Clear all Port D interrupt masks
    
  3. Set Pin Interrupt Masks: Unmask digital pins D3, D4, D5, and D6 (PCINT19 through PCINT22) in PCMSK2:

    PCMSK2 |= 0b01111000; // Enable PCINT on pins D3, D4, D5, and D6
    

Bitwise masking explicitly informs the microcontroller which digital pins must trigger the interrupt routine and which pins should be ignored. Masking unneeded pins prevents spurious interrupts from pin state toggles on unrelated I/O channels (such as serial UART communications on D0 and D1).

Basic framework for the pin change interrupt method, (code snippet)
void setup()
{
  PCICR |= 0b00000100;
  PCMSK2 = 0;
  PCMSK2 |= 0b01111000;

}

void loop()
{
  // Put your main code here, to run repeatedly:
}

// This is the interrupt service routine for the Port D pin change interrupt
// Nore: you cannot define the name for this ISR
ISR (PCINT2_vect){

}

Interrupts are now enabled exclusively on the four encoder channels: pins D3 through D6. Whenever a logic edge transition occurs on any of these masked pins, the Interrupt Service Routine vector ISR(PCINT2_vect) automatically triggers.

Note: PCINT2_vect is the hardware Interrupt Vector location for `Port D** on ATmega microcontrollers and must be defined explicitly in your Arduino code.

An efficient approach for decoding multiple quadrature encoders inside a single ISR is implementing a Lookup Table (LUT) state machine. The lookup table yields a direction value (\(+1\), \(-1\), or \(0\)) to increment, decrement, or leave the position counter unchanged by comparing the previous measured state against the current measured state of Channels A and B.To evaluate state transitions, construct a 4-bit composite state variable for each encoder structured as shown inFig 5:

Arrangement of the encoder data variable.
Arrangement of the encoder data variable.

Using this encoding, one can create the following table:

Prev. state Current state Resulting encoder variable, (above)RotationCount
00000000 = 0No0
00010010 = 1CCW-1
00100011 = 2CW1
00110100 = 3NA0
01000101 = 4CW1
01010110 = 5No0
01100110 = 6NA0
01110111 = 7CCW-1
10001000 = 8CCW-1
10011001 = 9NA0
10101010 = 10No0
10111011 = 11CW1
11011100 = 12NA0
11011101 = 13CW1
11101110 = 14CCW-1
11111111 = 15No0
Lookup tables for the encoder state and lookup table output.

Where: No = No rotation (current state = previous state), and NA = Not applicable (Impossible condition. Only one bit at a time can change)

Comparing Quadrature State Logic Tables

Analyse Table 2 to observe that exactly 8 state transition rows produce a valid count change (\(+1\) or \(-1\)), matching the 8 valid transition states in Table 1.

Compare the corresponding rows between both tables to understand how the 4-bit composite state values in Table 2 are mapped directly from raw binary pin transitions.

The resulting 4-bit binary state variable yields a decimal index between \(0\) and \(15\) (\(2^4 = 16\) total state combinations). This index directly queries a lookup table (LUT) array containing the transition values (\(+1\), \(-1\), or \(0\)) derived from Column 5 of Table 4. It is essential to retain all 16 entries—including invalid or non-adjacent state transitions—to maintain fixed-array indexing boundaries.

The lookup table array is initialized as:

static const int9_t lookup_table[] = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0};

The complete implementation script, DualEncoderUsingPinChangeInterrupts.ino, below, demonstrates dual quadrature encoder decoding across digital pins D3, D4, D5, and D6 using Port D Pin Change Interrupts.

Procedure:

  1. Using the technique described above, modify your Arduino code that you wrote for Exercise 2 to use the pin change interrupt for a single encoder attached to pins D2 and D3.
  2. When you have this working, try changing the encoder connection pins and modify your script accordingly.
Example code for the pin change interrupt method: DualEncoderUsingPinChangeInterrupts.ino
// Initialise the encoder counter variables
volatile long enc1_count = 0;
volatile long enc2_count = 0;

// Intialise a variable to to record the tick counter value
long tickValue = 0;

void setup() {

  PCICR |= 0b00000100;   // Activate the port D pin change interrupt on the
  // pin change interrupt control register
  PCMSK2 = 0;            // Reset the port D pin change mask
  PCMSK2 |= 0b01111000;  // Select the interrupt pins of interest from the
  // port D interrupt mask register

  // Define the pin modes dor D3, D4, D4, D6
  pinMode(4, INPUT);
  pinMode(5, INPUT);
  pinMode(6, INPUT);
  pinMode(7, INPUT);

  // Open the serial port at 9600 baud
  Serial.begin(9600);

  tickValue = millis();  // Record an initial tick counter value
}

void loop() {

  if (millis() >= tickValue + 100) {  // if 100ms has elapsed sine the last
  // time the followin code block ran

    tickValue = millis();  // Record the current tick counter value

    // Display the current encoder counter values on the serial monitor
    Serial.print("Encoder 1 Count = ");
    Serial.print(enc1_count);
    Serial.print("\tEncoder 2 Count = ");
    Serial.println(enc2_count);
  }
}

// This is the the interrupt service routine for the Port D pin change
// interrupt
// Note: you cannot define the name for this ISR
ISR(PCINT2_vect) {
  static const int8_t lookup_table[] = {
     0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0 };
  // Byte that stores the current and previous state of the encoder outputs
  // for encoder 1
  static uint8_t enc1_val = 0;
  // Byte that stores the current and previous state of the encoder outputs
  // for encoder 2
  static uint8_t enc2_val = 0;

  // Shift the previous current encoder value to the pervious position
  enc1_val = enc1_val << 2;

  // The following line is a compound instruction:
  // Read prot D and use an AND mask to remove bit information, then Shift
  // the data relating to D3 and D4 to the first 2 bits. OR this with the
  // enc1_val register to form the 4-bit current and previous encoder variable
  enc1_Val = enc1_val | ((PIND & 0b00011000) >> 3);

  // Add the lookup table value relating to enc1_val to the encoder 1 counter
  enc1_count += lookup_table[enc1_val & 0b00001111];

  // Shift the previous current encoder value to the pervious position
  enc2_val = enc2_val << 2;

  // The following line is a compound instruction:
  // Read prot D and use an AND mask to remove bit information, then Shift
  // the data relating to D5 and D6 to the first 2 bits. OR this with the
  // enc1_val register to form the 4-bit current and previous encoder variable
  enc2_Val = enc2_val | ((PIND & 0b01100000) >> 5);

  // Add the lookup table value relating to enc2_val to the encoder 2 counter
  enc2_count += lookup_table[enc2_val & 0b00001111];
}

Bitwise Shift Operators

Bit-shift operators manipulate variables directly at the binary bit level:

  • Right Shift (>>): Shifts all bits in an integer to the right by a specified number of bit positions. For example, y = x >> 3; shifts the binary representation of x three positions to the right and assigns the resulting value to y.
  • Left Shift (<<): Shifts all bits to the left by a specified number of bit positions (e.g., y = x << 2; shifts the bits of x two positions to the left).

Kinematic Conversion

After calculating the total encoder count, convert this value into encoder or motor output shaft revolutions, or angle, using the appropriate scaling constants.