We divided the software among two PICs. One PIC was called the Leader PIC and handled the majority of the sensing and gameplay logic while the other PIC was called the Motor PIC and handled drive and spoon motor control (this PIC was a Follower PIC). The two PICs communicated through what has been referred to as "semi-dummy SPI". To elaborate, the Leader PIC sent commands to the Motor PIC in traditional SPI fashion. However, since there were only two events that the Leader PIC could receive from the Motor PIC, the Motor PIC simply raised a shared line high for 2 seconds to indicate that the event occurred (since the software was written such that the Leader PIC was never in a state in which it was expecting both events, the same line could be used for both events).
Leader PIC
The Leader PIC was the main PIC in the software. This PIC was responsible for handling all gameplay logic, decision making, YARD indication, and almost all sensing. To handle the complexity of the software requirements, the Leader PIC implemented an HSM containing 3 levels. Each level is discussed/diagramed in depth below.
Top Level
The main function of the top level was to handle the start and stop of a game (mainly the stop). Upon entering the Idle State, the software lies in wait of the Gameplay switch being flipped from low to high, thus leading to the entry of the Gameplay State, which contained the next lower level. By having the Gameplay SM as a lower level, a round could be ended whilst in any substate of the Gameplay SM without needing to add an if-statement to every substate to handle the end of a round.
A round was supposed to last 2 minutes and 18 seconds. Since Framework timers can only be set to a maximum of 65 seconds, the software had to keep track of how many times the Gameplay timer timed out and reset the timer if not enough timeouts had occurred such that 2 minutes and 18 seconds had elapsed.
Gameplay Level
The Gameplay Level/SM contained the bulk of the code on the Leader PIC. This SM broke the game into strategical states that could be handled by the software.
The entry state handled determination of which YARD the robot was in. Our strategy was to rotate until the Leader PIC located all 4 beacons. The time of detection was recorded, and the change in time between seeing beacons E and D as well as A and K was computed. Since the robot was placed towards the back of a YARD at start, then the longest of these two time changes was the side that the robot was on.
After determining which YARD the robot was in, the robot needed to hit the ACORN that was placed near it at the start. It was determined that a lower level was needed to execute this state (see FirstACORN Level for a more in-depth discussion of this sub-level). Our strategy was to place the ACORN on whatever side of the robot was closest to the left wall of the YARD it was in. After determining which YARD it was in, the robot would rotate CCW until the proximity sensor detected the ACORN, stop to hit the ACORN, and then continue rotating until spotting the first TREE (after determining which YARD the robot was in, the DetermineYARD state labeled the rightmost TREE as FirstTREE and the leftmost TREE as SecondTREE - this assignment of TREEs allowed for more consistent behavior that was easier to handle). To avoid getting caught in an infinite loop either from the proximity sensor failing or the ACORN being accidentally placed too far for the robot to detect/hit, the robot was programmed to begin searching for FirstTREE after 5 seconds, thus allowing it to exit the FirstACORN state even if the first ACORN was never found.
After locating FirstTREE, the robot transitioned into the FindingFirstTREE state. As could likely be surmised from the previous sentence, the name of this state is a misnomer (our plan of how to find/approach FirstTREE changed while the code was being written, and the name is a holdover from a previous software implementation strategy of doing so), and would be more accurately called ApproachingFirstTREE. Our strategy of approaching FirstTREE was to rotate slightly past FirstTREE (this was implemented by use of a timer), then driving straight until tape was detected (tape detection was handled by the MotorPIC), then rotating CW until FirstTREE was detected again, and then driving straight until the ultrasonic sensor for TREEs detected that the robot was close enough to the TREE. This strategy was adopted since it allowed us to bypass the need to depend on tape sensing for the entire duration of driving to a TREE (we could get away with this since the motor speed control law on Motor PIC was so well tuned, that the robot could reliably drive straight for half a YARD length) and since it resulted in a path with a decently minimized length from starting position to the TREE. The reason why we did not drive straight to the TREE was on account of the fact that our shooting mechanism required the robot to be directly facing the YARD's wall when hitting ACORNs from TREEs, and driving straight from the starting position would have put the robot at an angle to the wall.
The AtTREE state was designed such that it could be used for either TREE. The preceeding state would set the TREE number (1 for FirstTREE or 2 for SecondTREE) as well as inform the Motor PIC as to which way to spin the Spoon Motor. The AtTREE state merely waited for the Motor PIC to inform the Leader PIC of the SpoonMotor stopping before using the TREE number to determine which state to go to next.
The last two states (FindingSecondTREE state and Defense state) were functionally similar, only differing by which TREE the robot was looking for. The purpose of this was to allow reuse of the FindingSecondTREE state after both TREEs were emptied such that the robot would be driving back and forth between the two TREEs - searching for ACORNs - after both TREEs were emptied. It would have been preferred to write a lower level SM to handle the post-TREE stage of the game, but time was unfortunately not on our side, hence necessitating a more basic reuse of code. In both of these states, the robot spun until the desired TREE was located (FindingSecondTREE was looking for SecondTREE, Defense was looking for FirstTREE). After locating the desired TREE, the robot would drive straight for 2 seconds before rotating to search for the desired TREE again. This was needed since it was possible to detect the desired TREE while barely not facing it head on, thus causing our robot to not drive straight towards the TREE (remember, we struggled with tape sensing, so this is the pitfall of not using tape sensing to drive along the tape). It was found that we needed to repeat this sequence once more. After this repetiion, the ultrasonic sensor was enabled to begin searching for a TREE (the robot would exit the state once this sensor detected that the robot was close enough to the TREE). While heading to the TREE, the ACORN proximity sensor was enabled such that, in the event an ACORN was detected, the robot would be able to command the Motor PIC which way to spin the Spoon Motor such that the ACORN would be hit into the opponent's YARD.
FirstACORN Level
Because of our decison to drive towards FirstTREE regardless of whether the FirstACORN had been found/hit, a lower level was needed for this state in order to facilitate easy exit from both the Searching and Hitting states. The Searching state consisted of the robot spinning CCW until the ACORN was detected. This event would cause the Leader PIC to command the Motor PIC to stop the drive motors and to spin the Spoon Motor CW once. Once the Motor PIC had informed the Leader PIC that it had stopped the Spoon Motor, the Leader PIC would return to the Searching state. This decision was made since it was observed that the proximity sensor was prone to noise that would cause it to falsely register that the ACORN was detected. Thus, the act of having it always return to Searching greatly improved our odds at successfully detecting the first ACORN.
Software
Motor PIC (Follower)
The Motor PIC handled control of the drive motors and the Spoon Motor. The only sensors that this PIC interactd with were the encoders for the drive motors and the tape sensors. Both of these were placed on this PIC since it was desired to only use SPI to send general motor commands.
The simplistic nature of the Motor PIC meant that an FSM could be used instead of an HSM. The main state machine for this PIC was MotorPIC_SM, which received all motor commands and handled them accordingly. A second FSM called SpoonMotorFSM was also utilized to help reduce complexity of code. Both state machines are discussed/diagramed in depth below.
MotorPIC_SM
Most of the time, this state machine was in the Waiting4CMD state. In this state, the PIC could respond to all motor commands sent to it by the Leader PIC.
When a command for the drive motors was received, a function from MotorService.c was called that selected a setpoint for each motor based off the command. MotorService.c implemented closed loop PID control with encoders to ensure the motors reliably hit the setpoint.
When a command for the Spoon Motor was received, an event was posted to SpoonMotorFSM that would spin the motor in the correct direction. Once the Spoon Motor had stopped, SpoonMotorFSM would post an event to MotorPIC_SM informing it of this, thus triggering MotorPIC_SM to raise the shared line with the Leader PIC high for 2 seconds to pass this information on to the Leader.
To handle the portion where the robot drove straight until detecting tape, a Driving2Tape state was implemented that would be transitioned to after a 1 second delay from the Motor PIC receiving the appropriate command (this delay was implemented since we didn't want the robot to detect the tape that was located at the starting position). The robot would drive straight upon entering this state and only exit once tape was detected. The transition back to Waiting4CMD state would also raise the shared line high for 2 seconds to inform the Leader PIC that tape had been found.
An Inactive state was also added where the robot would stop driving and had no capability of responding to any commands from the Leader PIC. This state was only transitioned to at the end of a match and was implemented to ensure that the robot stopped moving when desired.
SpoonMotor_FSM
Functionally, we needed the Spoon Motor to fulfill 4 requirements - spin CW once, spin CCW once, spin CW 5 times, and spin CCW 5 times. We found that it was possible to implement this with only two states - a NotSpinning state and a Spinning state. There were 4 possible ways to transition from NotSpinning to Spinning - one for each requirement - the only difference between the transitions being which way the spoon motor was spun and the length of time in which a voltage was applied to the spoon motor - controlled by SPOON_RETRACT_TIMER. Once SPOON_RETRACT_TIMER timed out, the Spoon Motor was stopped and MotorPIC_SM was informed of this.
Software