NORMAN's software is organized as a hierarchical state machine (HSM). At the top level, the GamePlayHSM coordinates the major phases of gameplay. Beneath it, sub-FSMs manage specific behaviors such as initialization, collection, tape-bucket scoring, and beacon-bucket scoring. Reusable services handle tasks such as tape following, servo motion, beacon interpretation, and communication between the two PICs. At the lowest level, HAL and module layers interface directly with motors, servos, encoders, SPI, analog-to-digital conversion (ADC), and input capture hardware.
The follower PIC packages all sensor information into a 16-bit code that is transmitted to the leader PIC over SPI. Each bit represents whether a particular sensor is active. The leader PIC decodes these bits and converts them into high-level events used by the robot’s state machines.
Click the button below to view the complete repository on Bitbucket. There you can find the full code listings and header files. The main project files are located in the ProjectHeaders and ProjectSource directories.
The C code was developed in MPLAB and programmed onto the PIC32 using a SNAP programmer.
Table of Contents
The GamePlayHSM is the top-level controller for the robot. It manages the overall sequence of gameplay by deciding which major behavior should run at any given time. Rather than directly handling low-level motor or sensor logic, it delegates that work to lower-level sub-FSMs: GameInitFSM, GameCollectFSM, GameTapeFSM, and GameBeaconFSM. The HSM listens for completion events from these sub-FSMs and uses them to transition between major phases of the game.
INITIALIZE:
CurrentState = GAME_INIT
tapeBucketCompleted = 0
beaconBucketCompleted = 0
FUNCTION InitGamePlayHSM(priority):
Save service priority
Initialize motors
Set initial event to ES_ENTRY
Start GamePlayHSM in GAME_INIT
Return success
FUNCTION PostGamePlayHSM(event):
Post event into GamePlayHSM queue
Return success or failure
FUNCTION RunGamePlayHSM(event):
Set MakeTransition = false
Set NextState = CurrentState
SWITCH on CurrentState:
STATE GAME_INIT:
Pass event into GameInitFSM through DuringGameInit()
If returned event is ES_INIT_DONE:
Transition to GAME_COLLECT
STATE GAME_COLLECT:
Pass event into GameCollectFSM through DuringGameCollect()
If returned event is ES_COLLECT_DONE:
If tapeBucketCompleted < 3:
Transition to GAME_TAPE
Else:
Transition to GAME_BEACON
STATE GAME_TAPE:
Pass event into GameTapeFSM through DuringGameTape()
If returned event is ES_TAPE_BUCKET_DONE:
Increment tapeBucketCompleted
Transition to GAME_COLLECT
STATE GAME_BEACON:
Pass event into GameBeaconFSM through DuringGameBeacon()
If returned event is ES_BEACON_BUCKET_DONE:
Increment beaconBucketCompleted
Transition to GAME_COLLECT
IF MakeTransition is true:
Send ES_EXIT to current state
Update CurrentState = NextState
Send ES_ENTRY to new state
Return ES_NO_EVENT
DURING FUNCTIONS:
DuringGameInit:
If event is ES_ENTRY:
Initialize and start GameInitFSM
If event is ES_EXIT:
Let GameInitFSM clean up
Stop motors
Otherwise:
Forward event to GameInitFSM
Return whatever event GameInitFSM gives back
DuringGameCollect:
If event is ES_ENTRY:
Start GameCollectFSM
If event is ES_EXIT:
Let GameCollectFSM clean up
Stop motors
Otherwise:
Forward event to GameCollectFSM
Return whatever event GameCollectFSM gives back
The GameInitFSM is responsible for preparing the robot at the start of the match. Its primary goal is to determine which arena the robot starts in and then position the robot on the tape network so normal gameplay can begin.
INITIALIZE:
CurrentState = GI_ROTATING_CW
Allow_TJunctions = false
FUNCTION StartGameInitFSM(event):
Set CurrentState to GI_ROTATING_CW
Reset Allow_TJunctions
Run state machine with entry event
FUNCTION RunGameInitFSM(event):
SWITCH(CurrentState)
STATE GI_ROTATING_CW:
On ENTRY:
Set flag servo to neutral
Rotate robot clockwise
On ES_IN_ARENA_GREEN:
Stop motors
Raise green flag
Transition to GI_ROTATING_CCW
On ES_IN_ARENA_BLUE:
Stop motors
Raise blue flag
Transition to GI_ROTATING_CCW
STATE GI_ROTATING_CCW:
On ENTRY:
Rotate robot counter-clockwise
On ES_MIDDLE_TAPE_ONLY:
Stop motors
Transition to GI_MOVE_FWD
STATE GI_MOVE_FWD:
On ENTRY:
Move forward
Enable tape following
Start timer to ignore early junction detections
On timer timeout:
Allow T-junction detection
Raise collector arm
On ES_TJUNCTION (if allowed):
Stop motors
Transition to GI_ROTATING_CW_90
On EXIT:
Disable tape following
STATE GI_ROTATING_CW_90:
On ENTRY:
Rotate clockwise
On ES_MIDDLE_TAPE_ONLY:
Stop motors
Post ES_INIT_DONE to GamePlayHSM
The GameCollectFSM controls how NORMAN positions itself at the dispenser and collects balls. Once in position, it begins the collection sequence by repeatedly actuating the paddle servo to knock balls out of the dispenser and into the robot
INITIALIZE:
CurrentState = GC_MOVE_FWD
NUM_OF_WACKS = 0
FUNCTION StartGameCollectFSM(event):
Set CurrentState to GC_MOVE_FWD
Run state machine with entry event
FUNCTION RunGameCollectFSM(event):
SWITCH(CurrentState)
STATE GC_MOVE_FWD:
On ENTRY:
Enable tape following
Move forward
On ES_TJUNCTION:
Stop motors
Lower collection arm
Transition to GC_MOVE_CLOCKWISE_TINY
On EXIT:
Disable tape following
STATE GC_MOVE_CLOCKWISE_TINY:
On ENTRY:
Rotate clockwise slightly
Start short timer
On timer timeout:
Stop motors
Transition to GC_MOVE_BACKWARDS
STATE GC_MOVE_BACKWARDS:
On ENTRY:
Move backwards into dispenser
Start backwards timer
On timer timeout:
Stop motors
Transition to GC_COLLECTING_COAL
STATE GC_COLLECTING_COAL:
On ENTRY:
Reset number of wacks to 0
Move forward slightly
Start collection timer
On AllPurpose timer timeout:
If number of wacks < 7:
Move paddle servo to 180 degrees
Increment number of wacks
Start reset timer
Else:
Post ES_COLLECT_DONE to GamePlayHSM
On Reset timer timeout:
Move paddle servo back to 0 degrees
Restart collection timer
The GameTapeFSM controls how NORMAN navigates to and scores in the tape-guided buckets. After the collection sequence finishes, this FSM uses tape following and junction detection to guide the robot from the dispenser to the correct bucket, stop at the bucket, dump the balls, and then return to the dispenser area.
INITIALIZE:
CurrentState = GT_MOVE_FWD
buckets = CurrentBucketCount() + 1
Stage = 1
returning = false
FirstT = true
FUNCTION StartGameTapeFSM(event):
Set CurrentState to GT_MOVE_FWD
Set target bucket based on current bucket count
Reset stage tracking variables
Run state machine with entry event
FUNCTION RunGameTapeFSM(event):
SWITCH(CurrentState)
STATE GT_MOVE_FWD:
On ENTRY:
Move forward
Enable tape following
On ES_BACK_RIGHT_DETECTED:
If Stage == 1:
If target bucket is not bucket 1:
Stop motors
Transition to GT_ROTATING_CW_90
Advance Stage
Else:
Post ES_TAPE_BUCKET_DONE
On ES_BACK_LEFT_DETECTED:
If Stage == 3:
Stop motors
Count left junction
Transition to GT_ROTATING_CCW_90
On ES_TJUNCTION:
If Stage == 1 or Stage == 5:
Stop motors
Transition to GT_DISPENSING
Advance Stage
On EXIT:
Disable tape following
STATE GT_ROTATING_CW_90:
On ENTRY:
Rotate clockwise
On ES_MIDDLE_TAPE_ONLY:
Stop motors
If returning is false:
Transition to GT_MOVE_FWD
Else:
Transition to GT_MOVE_BACKWARDS
Advance Stage
STATE GT_ROTATING_CCW_90:
On ENTRY:
Rotate counter-clockwise
On ES_MIDDLE_TAPE_ONLY:
Stop motors
If returning is false:
Transition to GT_MOVE_FWD
Advance Stage
Else:
Post ES_TAPE_BUCKET_DONE
STATE GT_DISPENSING:
On ENTRY:
Stop motors
Raise dumping arm
Start dispensing timer
On dispensing timer timeout:
Lower dumping arm
Transition to GT_MOVE_BACKWARDS
Advance Stage
STATE GT_MOVE_BACKWARDS:
On ENTRY:
Drive backwards away from bucket
On ES_TJUNCTION:
If not returning from bucket 1:
Stop motors
If this is the first T-junction on return:
Transition to GT_ROTATING_CCW_90
Mark first T handled
Else:
Mark robot as returning to dispenser
Transition to GT_ROTATING_CCW_90
Reset first-T flag
Advance Stage
On ES_BACK_RIGHT_DETECTED:
If returning from bucket 1:
Stop motors
Post ES_TAPE_BUCKET_DONE
On EXIT:
Disable tape following
The GameBeaconFSM controls how NORMAN navigates to and scores in the two beacon-side buckets after the three tape buckets have been completed. Unlike the tape-bucket routine, this FSM relies more heavily on preset distance moves and turns to reach the bucket locations.
INITIALIZE:
CurrentState = GB_MOVE_FWD
returning = false
first = true
atBucket = false
FirstT = true
FUNCTION StartGameBeaconFSM(event):
Set CurrentState to GB_MOVE_FWD
Reset path-tracking variables
Run state machine with entry event
FUNCTION RunGameBeaconFSM(event):
SWITCH(CurrentState)
STATE GB_MOVE_FWD:
On ENTRY:
Move forward
On ES_BACK_RIGHT_DETECTED:
Stop motors
Transition to GB_ROTATING_CW_90
On ES_DISTANCE_MET:
Stop motors
If robot has not yet reached bucket lane:
Transition to GB_ROTATING_CCW_90
Mark that next forward move reaches bucket
Else:
Transition to GB_DISPENSING
On EXIT:
Disable tape following
STATE GB_ROTATING_CW_90:
On ENTRY:
Rotate clockwise 90 degrees
On ES_DISTANCE_MET:
Stop motors
If going to first beacon bucket:
Move forward preset short distance
Mark first bucket handled
Else if going to second beacon bucket:
Move forward preset long distance
Reset first-bucket flag
If returning is false:
Transition to GB_MOVE_FWD
Else:
Transition to GB_MOVE_BACKWARDS
STATE GB_ROTATING_CCW_90:
On ENTRY:
Rotate counter-clockwise 90 degrees
On ES_DISTANCE_MET:
Stop motors
If going toward bucket:
Move forward preset distance into bucket lane
Transition to GB_MOVE_FWD
Else if returning:
Post ES_BEACON_BUCKET_DONE to GamePlayHSM
STATE GB_DISPENSING:
On ENTRY:
Stop motors
Mark returning = true
Start dispensing timer
On dispensing timer timeout:
Stop motors
Transition to GB_MOVE_BACKWARDS
STATE GB_MOVE_BACKWARDS:
On ENTRY:
Move backwards away from bucket
On ES_TJUNCTION:
Stop motors
If this is the first T-junction on return:
Transition to GB_ROTATING_CW_90
Mark first T handled
Else:
Transition to GB_ROTATING_CCW_90
Reset first-T flag
On EXIT:
Disable tape following
The PIC Leader acts as the main communication bridge between the follower PIC and the rest of the robot software. Its job is to repeatedly query the follower over SPI, receive a 16-bit sensor code, and convert that raw code into meaningful gameplay events.
INITIALIZE:
Configure SPI2 as leader
Set SPI transfer width to 16 bits
Initialize motors and servos
Start a periodic query timer
FUNCTION InitPICLeaderService(priority):
Save service priority
Configure SPI pins and settings
Initialize motor and servo hardware
Start query timer
Post ES_INIT
Return success
FUNCTION PostPICLeaderService(event):
Post event into PIC Leader queue
Return success or failure
FUNCTION RunPICLeaderService(event):
If event is QUERY_TIMER timeout:
Send 16-bit query word over SPI
Read 16-bit sensor code from follower PIC
Store received code
Convert front tape bits into one tape-following event
If a front tape event exists:
Post it to TapeService
Restart query timer
If event is ES_NEW_CODE:
Look at the beacon bits in the received code:
If green beacon detected:
Post ES_BEACON_G_DETECTED to BeaconService
Else if blue beacon detected:
Post ES_BEACON_B_DETECTED to BeaconService
Else if left beacon detected:
Post ES_BEACON_L_DETECTED to BeaconService
Else if right beacon detected:
Post ES_BEACON_R_DETECTED to BeaconService
Else:
Post ES_NO_BEACON to BeaconService
Look at front tape sensor bits:
Convert front sensor combinations into events like:
ES_MIDDLE_LEFT_TAPE
ES_MIDDLE_RIGHT_TAPE
ES_MIDDLE_TAPE_ONLY
ES_LEFT_TAPE_ONLY
ES_RIGHT_TAPE_ONLY
Post these events to GamePlayHSM
Look at rear tape sensor bits:
If both rear sensors detect tape:
Post ES_TJUNCTION to GamePlayHSM
Else if left rear detects tape:
Post ES_BACK_LEFT_DETECTED to GamePlayHSM
Else if right rear detects tape:
Post ES_BACK_RIGHT_DETECTED to GamePlayHSM
The TapeService is responsible for keeping NORMAN centered on the tape while driving. It receives front tape sensor events from the PIC Leader Service and converts them into a discrete tracking error. For example, ES_LEFT_TAPE_ONLY corresponds to a larger left error, while ES_MIDDLE_TAPE_ONLY corresponds to zero error.
This error is used in a fixed-point PD controller to compute a small steering correction. The controller adjusts the left and right motor commands relative to a stored base speed, which allows the robot to smoothly correct its position without overreacting.
INITIALIZE:
CurrentState = InitTapeState
EnableFlag = false
baseLeft = 0
baseRight = 0
prevErr = 0
FUNCTION InitTapeService(priority):
Save service priority
Set initial state to InitTapeState
Post ES_INIT
Return success
FUNCTION PostTapeService(event):
Post event into TapeService queue
Return success or failure
FUNCTION RunTapeService(event):
SWITCH(CurrentState)
STATE InitTapeState:
On ES_INIT:
Transition to WaitForEvents
STATE WaitForEvents:
If TapeService is disabled:
Do nothing
Convert tape event into error:
left only -> -2
middle+left -> -1
middle only -> 0
middle+right -> +1
right only -> +2
If event is not a tape-following event:
Do nothing
If error == 0:
Update base motor speeds
Reset previous error
Command motors to drive at base speed
Else:
Compute derivative term:
derr = error - prevErr
Select PD gains based on forward or reverse mode
Compute control correction:
u = Kp * error + Kd * derr
Clamp correction to a maximum value
If in reverse mode:
flip correction sign
Apply correction to motor speeds:
left = baseLeft + u
right = baseRight - u
Clamp motor speeds to valid range
If motor command has changed:
send new speeds to MotorModule
Save current error as previous error
The BeaconService interprets beacon-detection events and determines which arena NORMAN starts in. It listens for beacon events generated by the PIC Leader and converts them into higher-level gameplay events that the GamePlayHSM can use.
INITIALIZE:
CurrentState = InitBeaconState
FirstBeacon = none
BlueArena = false
FUNCTION InitBeaconService(priority):
Save service priority
Set initial state to InitBeaconState
Post ES_INIT
Return success
FUNCTION PostBeaconService(event):
Post event into BeaconService queue
Return success or failure
FUNCTION RunBeaconService(event):
SWITCH(CurrentState)
STATE InitBeaconState:
On ES_INIT:
Transition to WaitForFirstBeacon
STATE WaitForFirstBeacon:
On ES_BEACON_L_DETECTED:
Save that left beacon was seen first
Post ES_IN_ARENA_GREEN to GamePlayHSM
Transition to Chill_State
On ES_BEACON_R_DETECTED:
Save that right beacon was seen first
Post ES_IN_ARENA_BLUE to GamePlayHSM
Transition to Chill_State
On ES_BEACON_G_DETECTED or ES_BEACON_B_DETECTED:
Ignore for current strategy
STATE Chill_State:
Ignore all future beacon events
The Motor HAL provides a simple low-level interface for driving the robot’s motors and reading encoder motion. It hides the hardware details of PWM generation, direction control, and encoder interrupts so that higher-level modules can command motor behavior using simple calls like “set left motor speed” or “reset encoder counts.”
INITIALIZE:
LeftTicks = 0
RightTicks = 0
FUNCTION motorInit():
Configure motor control pins as digital outputs
Configure Timer 2 for PWM generation
Configure Output Compare channels for left and right motors
Map PWM outputs to the correct motor pins
Initialize both motors to 0 duty cycle
FUNCTION motorSetSpeed(whichMotor, dutyCycle):
Clamp dutyCycle to range [-100, 100]
Convert duty cycle into PWM compare value
If motor is LEFT:
If dutyCycle > 0:
Set left motor direction to forward
Apply PWM value
Else if dutyCycle == 0:
Stop left motor
Else:
Set left motor direction to reverse
Apply PWM value
If motor is RIGHT:
If dutyCycle > 0:
Set right motor direction to forward
Apply PWM value
Else if dutyCycle == 0:
Stop right motor
Else:
Set right motor direction to reverse
Apply PWM value
FUNCTION EncoderInit():
Configure encoder input pins
Map encoder pins to external interrupts
Set interrupt edge polarity
Clear interrupt flags
Enable encoder interrupts
FUNCTION Encoder_LeftTicks():
Return current left encoder tick count
FUNCTION Encoder_RightTicks():
Return current right encoder tick count
FUNCTION Encoder_Reset():
Reset both encoder tick counts to 0
FUNCTION Encoder_ResetLeft():
Reset left encoder tick count to 0
FUNCTION Encoder_ResetRight():
Reset right encoder tick count to 0
INTERRUPT LeftEncoder_ISR:
Increment LeftTicks
INTERRUPT RightEncoder_ISR:
Increment RightTicks
The MotorModule provides higher-level motion control for the robot. While the Motor HAL directly controls PWM signals to the motors, the MotorModule translates meaningful robot actions, such as moving forward a specific distance, rotating a certain angle, or stopping, into motor commands.
INITIALIZE:
CommandedSpeed_Left = 0
CommandedSpeed_Right = 0
targetTicks = 0
active = false
FUNCTION InitMotorModuleService(priority):
Save service priority
Print initialization message
Post ES_INIT event
Return success
FUNCTION PostMotorModuleService(event):
Post event into MotorModule queue
Return success
FUNCTION MotorStop():
Set both motor speeds to 0
Update commanded speed variables
Disable distance monitoring
FUNCTION MotorEditSpeed(leftSpeed, rightSpeed):
Send speed commands to Motor HAL
Update commanded speed variables
FUNCTION MotorMoveForward(distance_mm, speed):
If distance is nonzero:
Convert distance to encoder ticks
Reset encoders
Enable distance monitoring
Command both motors forward at the requested speed
FUNCTION MotorMoveBackward(distance_mm, speed):
If distance is nonzero:
Convert distance to encoder ticks
Reset encoders
Enable distance monitoring
Command both motors in reverse
FUNCTION MotorRotateCW(angle_deg, speed):
Convert rotation angle into encoder ticks
Reset encoders
Enable distance monitoring
Set left motor forward
Set right motor reverse
FUNCTION MotorRotateCCW(angle_deg, speed):
Convert rotation angle into encoder ticks
Reset encoders
Enable distance monitoring
Set left motor reverse
Set right motor forward
FUNCTION RunMotorModuleService(event):
On DistanceTimer timeout:
If distance monitoring is active:
Read encoder ticks
Compute average tick count
If target distance reached:
Stop motors
Disable monitoring
Post ES_DISTANCE_MET to GamePlayHSM
Otherwise:
Restart timer to continue monitoring
Distance Conversion:
ticks = (distance_mm * TICKS_PER_REV) / (π * wheel_diameter)
Rotation Conversion:
ticks = (angle_deg * track_width * TICKS_PER_REV) /
(360 * wheel_diameter)
The Servo HAL provides a simple low-level interface for controlling the robot’s three servos: the arm servo, paddle servo, and flag servo. It hides the hardware details of PWM generation and angle-to-pulse conversion so that higher-level modules can command servos using only a desired angle.
INITIALIZE:
Define servo pulse-width ranges
Define maximum angle for each servo
Map each servo to its output compare register
FUNCTION servoInit():
Configure servo pins as digital outputs
Set up Timer 3 for 50 Hz PWM
Set prescaler
Set timer period
Start timer
Turn off OC modules during setup
Configure OC1, OC2, and OC5 for PWM mode
Set Timer 3 as the time base
Initialize duty cycle registers to 0
Map OC outputs to the correct servo pins:
OC1 -> arm servo
OC2 -> paddle servo
OC5 -> flag servo
Turn on all OC modules
FUNCTION servoSetAngle(whichServo, angle):
Convert requested angle into PWM ticks
Write tick value into that servo’s OCxRS register
FUNCTION servoAngleToTicks(whichServo, angle):
If servo is arm servo:
Clamp angle to 0–270 degrees
Convert angle linearly into pulse width ticks
Else if servo is paddle or flag servo:
Clamp angle to 0–180 degrees
Convert angle linearly into pulse width ticks
Return tick value
The ArmServoService controls the robot’s dumping arm by gradually moving the arm servo to a requested angle. Instead of instantly jumping to a new position, the service uses a speed-controlled motion profile so the arm moves smoothly and avoids jerky movements that could destabilize the robot or spill balls prematurely.
INITIALIZE:
targetAngle = 0
currentAngle = 0
currentSpeed = 0
FUNCTION InitArmServoService(priority):
Save service priority
Initialize servo hardware
Print initialization message
Post ES_INIT event
Return success
FUNCTION PostArmServoService(event):
Post event into ArmServoService queue
Return success
FUNCTION RunArmServoService(event):
SWITCH(event type)
EVENT ES_NEW_SERVO_ANGLE_REQUESTED:
Save requested target angle
Start servo speed timer
EVENT SERVO_SPEED_TIMER TIMEOUT:
Calculate difference between targetAngle and currentAngle
Determine motion direction
If far from target:
Gradually increase servo speed
If near target:
Gradually decrease servo speed
If inside deadband:
Stop motion
Limit speed to maximum allowed value
Update currentAngle toward target
Command servo to new angle using Servo HAL
Restart speed timer
This module provides helper routines for controlling the 8 NeoPixels mounted above the plug holes.
FUNCTION neopixel_HAL_init():
Initialize the NeoPixel driver
Clear the entire LED strip
Send an update so the LEDs begin turned off
FUNCTION random_infile(min, max):
Generate a random integer between min and max (inclusive)
Return the random integer
FUNCTION SetColor():
Generate a random red value
Generate a random green value
Generate a random blue value
Store these values for use in animations
FUNCTION PreGameLeds():
(Blocking animation used only for early testing)
Generate a random color
For each pixel in the LED strip:
Clear the strip
Set that pixel to the random color
Update the strip so animation appears on the LEDs
FUNCTION PreGameLeds_Step():
(Non-blocking animation used in IdleGame)
Generate a random color
Advance an internal pixel index (reset to 0 after reaching MAX_LEDS)
Clear the LED strip
Light only the current pixel using the random color
Update the strip to show this frame of animation
FUNCTION SetAllToRed():
Clear the LED strip
Set every pixel to red
Update the strip
FUNCTION SetAllToGreen():
Clear the LED strip
Set every pixel to green
Update the strip
FUNCTION SetAllToYellow():
Clear the LED strip
Set every pixel to yellow
Update the strip
FUNCTION InGameLeds(a, b):
Clear the LED strip
Light pixel “a” in blue
Light pixel “b” in blue
Update the strip so these two pixels are highlighted
This module provides the low-level driver functions for directly controlling the WS2812 (NeoPixel) LED strip using SPI.
FUNCTION neopixel_init():
Configure the SPI peripheral for NeoPixel timing requirements
Map the MOSI pin to the microcontroller output connected to the LED data line
Set the SPI bit timing, idle polarity, and sampling edge appropriately
Ensure the transfer width is 8 bits
Enable the SPI module
Initialize the internal LED color buffer to zeros
Clear the LED strip so all pixels start off
FUNCTION spi_send(byte):
Send a single byte over the configured SPI channel
Wait until the hardware indicates the byte has fully transmitted
HELPER FUNCTION encode_two_bits(bit_high, bit_low):
Convert two WS2812 bits (each either 0 or 1) into a single SPI byte
Use the WS2812 timing pattern:
– A “0” bit is represented by one specific 4-bit pattern
– A “1” bit is represented by another specific 4-bit pattern
Place the encoded “high bit” in the upper nibble
Place the encoded “low bit” in the lower nibble
Return the combined byte
HELPER FUNCTION send_color_byte(color_byte):
Split the 8 bits of a color byte into four 2-bit groups
For each pair of bits:
– Encode the two WS2812 bits into a single SPI byte
– Send that encoded byte out over SPI
FUNCTION neopixel_show():
For each LED in the strip:
– Retrieve its stored G, R, and B values from the internal buffer
– Send the green byte using send_color_byte()
– Send the red byte using send_color_byte()
– Send the blue byte using send_color_byte()
(WS2812 LEDs expect colors in G-R-B order)
After all LEDs have been sent:
– Transmit several zero bytes to create the required “reset” low time
FUNCTION neopixel_SetPixel(index, g, r, b):
If the LED index is out of range:
– Ignore the request
Otherwise:
– Store the supplied G, R, and B values in the internal LED buffer
FUNCTION neopixel_clear():
Set all entries of the LED color buffer to zero
The PIC Follower is responsible for reading the robot’s low-level sensors and packaging that information into a single 16-bit code that can be sent to the PIC Leader over SPI. It continuously monitors both the beacon sensor and the five tape sensors, then updates the corresponding bits in the code to indicate which sensors are currently active.
INITIALIZE:
Code = 0
Set all tape sensor readings to 0
Configure ADC channels for 5 tape sensors
Configure input capture for beacon frequency measurement
Configure Timer 2 for periodic tape sampling
Configure Timer 3 for beacon rollover timing
Configure SPI1 as follower in 16-bit mode
FUNCTION InitPICFollowerService(priority):
Save service priority
Initialize tape sensor hardware
Initialize beacon capture hardware
Initialize SPI follower interface
Start follower debug timer
Post ES_INIT
Return success
FUNCTION RunPICFollowerService(event):
On periodic timeout:
Print current 16-bit code
Check whether beacon signal has timed out
Update beacon classification based on measured frequency
Restart timeout timer
SPI INTERRUPT: SetDetection
Read incoming query word from leader
Write current 16-bit Code to SPI buffer
Clear SPI interrupt flag
INPUT CAPTURE INTERRUPT: Beacons
Read latest capture time
Handle timer rollover if needed
Compute period between captures
Convert period into beacon frequency
Store most recent frequency
TIMER 2 INTERRUPT: Tapes
Read all 5 tape sensor ADC values
If tape middle value > threshold:
Set middle tape bit
Else:
Clear middle tape bit
Repeat for:
left tape
right tape
back left tape
back right tape
FUNCTION UpdateBeacon():
Compare measured beacon frequency to:
Green beacon frequency
Blue beacon frequency
Left beacon frequency
Right beacon frequency
Find closest frequency
If closest frequency is within deadband:
Clear old beacon bits
Set only the matching beacon bit
FUNCTION BeaconTimeout():
If enough time has passed since the last beacon capture:
Clear all beacon bits
Set beacon frequency to 0
The CheckNewCode event checker monitors communication between the Follower PIC and the Leader PIC. The follower continuously sends a 16-bit code that represents the current state of its sensors, including tape sensors and beacon detectors.
The event checker compares the most recently received sensor code with the previous code. If the value has changed, it means that the robot’s sensor state has changed (for example, detecting tape, losing tape, or seeing a beacon). When this happens, the function posts an ES_NEW_CODE event to the PICLeaderService, which then decodes the sensor bits and generates the appropriate events for the rest of the system.
FUNCTION CheckNewCode():
Access global variables:
ReceivedCode // newest sensor code from follower PIC
LastCode // previously processed sensor code
IF ReceivedCode is different from LastCode:
Temporarily disable interrupts
Update LastCode with the new ReceivedCode
Re-enable interrupts
Create event:
EventType = ES_NEW_CODE
Post event to PICLeaderService
Return TRUE (event occurred)
ELSE
Return FALSE (no change detected)
Modified from lab to include logic for dealing with SPI2