Testing each part of the system

Each component of the system was tested independently and in isolation. This ensured that each module would work without issues once integrated into the full system. Initially, we developed on a RaspberryPi 2, since more resources were available regarding bare metal programming. We ended up sticking to the 2 for the final project as we didn't have time to port the code over to the RaspberryPi 3, as there were minor difference in startup code and configuration.

Bare Metal Startup

One of the biggest testing challenges was testing the bare metal startup code. Without a direct GDB or JTAG debugger, stack traces and other common debugging methods could not be used to find issues and ensure everything was working as expected. At first, the only debugging tools were the two LEDs on the RaspberryPi, which are hooked up to two GPIO pins. Once basic features like UART were implemented, serial debugging was added, which made things a lot easier.


Since half of the code for startup is written in assembly, every new feature presented a new set of difficult-to-find bugs. These included using hardware floating points, initializing interrupt handlers, and running multiple cores. Most of these issues were addressed through large print statements to occasionally dump register values, blinking LEDs when expected behavior occured, and feverishly searching the RaspberryPi Bare Metal forums and GitHub for examples of working code that accomplished the same goal as was desired.

Motion Control Testing

Motion control was developed with incremental testing as new features were added. Each of the major milestones were:

  1. Send step and direction pulses
  2. Set up velocity control of the motors
  3. Set up encoder feedback
  4. Implement a PID control loop
  5. Implement constant feed-rate motion in one dimension
  6. Implement constant feed-rate motion in two dimensions

Before moving onto the next milestone, a milestone had to be completely tested and proven to work. This allowed future milestone development to assume that all previous milestones function as expected, limiting the posible failure modes when testing that milestone.

Testing of 1-3 was done by writing test programs to run the motor and observing motor/encoder behavior, and confirming that pulses were being sent and measured as expected with an oscilloscope.

4-6 were tested more qualitatively than 1-3. Once the code was written, a simple program would move the motor and we would observe if the behavior was as expected. For example, when testing two-dimensional constant feed-rate motion, we would move the stage with a pen attached to the Z-axis that would draw on a piece of paper. We would confirm that the resulting line was straight (and not curved or jagged), concluding that the speed of each axis was calculated properly.


The main issue discovered when developing motion control was encoder jitter. When stationary, the pulse width output by the encoder was not perfectly constant, but had small amounts of jitter. This, coupled with error in measurement, led to noise in the angle value. This, in turn, would cause the PID loop to try to componesate for the noise and start causing the axis to jitter slightly when it was supposed to be stationary.

Path Planning

Path planning was developed piecewise to ensure that each part worked in step. GCode parsing was developed as a switch case statement. This made it rather easy to test parsing piecewise as it would fall into given cases and can be handled selectively. It generally worked as expected here.


Interpolation had the first main issue that needed to be worked out. As planning was developed before the bare metal environment was fully set up, floating point mathematics still took a long time. To get around this, the parser was developed to return ints which were not as computationally expensive. As floating point math is traditionally a problem with microcontrollers, the team used a function from the grbl codebase, read_float to extract numbers from strings for integer math.