Projects
-
asp Assembler
March 2025, Personal Project
Assemble machine code for a stepper motor ASIP. Written in Rust.
-
CAN Driver and App Layer
Fall 2024, MAC Formula Electric FSAE
An interrupt driven, thread-safe CAN system using modern C++.
-
Platform Abstracted Peripherals
2023 - 2024, MAC Formula Electric FSAE
Implemented a common interface for peripherals to enable cross-platform execution and testing.
-
Cross Platform Build System
2023, MAC Formula Electric FSAE
Compiles firmware for various platforms from a single build system.
-
Firmware Documentation Website
2024 - Present, MAC Formula Electric FSAE
Comprehensive and beautiful documentation for my firmware team. Written in Markdown.
-
Coding Playground
2024 - Present, Personal Project
Small projects to prototype ideas and practice new languages.
-
Painter's Grip Pro
Spring 2021, Top First Year Engineering Project
A smart assistive paintbrush which detects hand tremors to avoid fibromyalgia flareups.
-
All of the Lights
DeltaHacks 8 (2022), Best Hardware Hack
Controlled RGB LED strips with MOSFETs & PWM using UART commands from a Raspberry Pi web server.
-
Sumobot
McMaster Sumobot 2021, Best Hardware Design
Integrated sensors & motors with an Arduino inside a 3D printed chassis.
-
Serial Communication with LED
May 2021, Personal Project
Sending data between Arduinos with an LED and photoresistor.
-
FET H-Bridge PCB
Spring 2019, Personal Project
Designed an H-Bridge circuit for a DC motor. Used toner-transfer and acid-etching to make my PCB.
3D Printing Arduino Bash C++ CMake Electronics GitHub Markdown Python Raspberry Pi Rust STM32
asp Assembler
March 2025, Personal Project
Skills: Rust
See the asp repo.
CAN Driver and App Layer
Fall 2024, MAC Formula Electric FSAE
Skills: C++, STM32
Outcomes
-
Uses interrupts to receive messages instead of relying on polling.
No incoming messages are ever missed. Reduces stack memory by handling less data, more often.
-
Represents missing and consumed data with C++
std::optional
rather than "null-value" conventions.Prevents the misuse of invalid data. Inspired by Rust's
Option
type. -
Message reception and processing are thread and interrupt safe by using atomic variables.
The same layer can be used in hardware (stm32) and software simulation (SIL).
Interrupts vs Polling

The STM32 CAN peripheral has a 3-slot FIFO queue which automatically receives messages from the CAN bus. It is firmware's responsibility to empty the queue sufficiently quickly to avoid "overrun" which causes new messages to be dropped.
Prior to my development, the FIFO messages were handled by .ReadQueue()
which emptied the FIFO and made the messages available to the application. This method risks losing data via FIFO overrun if the CAN bus receives messages faster than the application calls .ReadQueue()
.
My changes use the "Message Received" interrupt (CAN_IT_RX_FIFO0_PENDING
) to send incoming messages all the way up to the App Layer object, where it is binned by ID and made available for firmware.
Technical Note
The previous system actually did use interrupts, but not to their full extent. A hardware interrupt would automatically empty the hardware FIFO into a software queue and the app would poll .ReadQueue()
to bring up the messages from the SW queue.
While this technically uses interrupts, the SW queue could still be overrun, so it was as if interrupts weren't being used in the first place.
The only benefit to the SW queue was it had 20 slots compared to 3 in the HW FIFO, so this solution only delayed overrun instead of preventing it.
Message Containers with std::optional
C++17 introduced the "optional" data type which represents a value which may or may not exist. This provides an elegant and correct way to store messages on a CAN bus since at some points in time, a message may not have yet arrived.
std::optional
replaces the common practice of indicating missing values with 0 or -1, or by maintaining a separatedata_is_valid
boolean alongside somedata
.
If GetXMsg()
is called before an [X]
message has arrived, the App layer will return std::nullopt
. Without an optional
value, the method would be forced to return an invalid XMsg
object which could be misused by the application.
In this example, we determine if the FSAE battery is in the "open" state from a CAN message. If no message has arrived, we should return false
. Compare the code before and after my update.
bool IsContactorsOpen() {
ContactorStates msg;
can.Read(msg);
return msg.timestamp != 0 &&
msg.pack_positive == State::OPEN &&
msg.pack_negative == State::OPEN &&
msg.pack_precharge == State::OPEN;
}
bool IsContactorsOpen() {
auto msg = can.GetRxContactorStates();
if(msg.has_value()) {
return msg->PackNegative() == State::OPEN &&
msg->PackPositive() == State::OPEN &&
msg->PackPrecharge() == State::OPEN;
} else {
return false;
}
}
Notice how much more explicit the std::optional
solution is. Instead of representing message validity with a convention on the .timestamp
field, the new solution requires the developer to explicity indicate the desired behaviour.
Memory Safety
What happens if firmware is reading a message via GetYMsg()
, but the interrupt pushes a new Y
message to the App layer? This could corrupt the data mid-read. A single container will not support synchronous reading and writing.
I wrote a SPSC buffer with two containers "left" and "right." The producer (interrupt) always writes to the opposite side from where the consumer (firmware) is reading, preventing data corruption.
This is synchronized with an atomic flag, providing a cross-platform solution. After the producer finishes adding a new message, it flips the flag, directing the next reader to consume the newly written data but without interrupting any existing read operations.
Mutexes are not an option on bare metal microcontrollers, and even if they were, it's bad practice to block a interrupt. Also, if this system is to be cross-platform, it cannot rely on interrupts since this would have no effect on our software-in-the-loop Linux server (see Platform Abstracted Peripherals).
Platform Abstracted Peripherals
2023 - 2024, MAC Formula Electric FSAE
Skills: C++, STM32
Peripheral interface definitions https://github.com/macformula/racecar/tree/main/firmware/shared/periph
Platform peripheral drivers (ex
stm32/periph
) https://github.com/macformula/racecar/tree/main/firmware/mcal/Full architecture description https://macformula.github.io/racecar/firmware/architecture/
I started writing stm32
firmware for MAC Formula Electric while on co-op and quickly recognized the need to test code without access to the physical vehicle. This inspried me to design a peripheral interface system that is abstracted from any specific platform, so it can be tested on any platform.
Outcomes
- App-level code is completely isolated from any platform (there are no HAL commands at the app level).
- Before testing on the vehicle, we can debug logic:
- In the command line using the
cli
platforms (I/O throughstdin
andstdout
). - On our automated SIL test server (I/O through server GET / PUT).
- In the command line using the
- Old projects can be ported to new platforms almost trivially.
My procedure
- Wrote C++ drivers for the first two platforms' peripherals (ADC, PWM, GPIO)
- STM32 peripherals using the STM HAL.
- Command Line Interface drivers using
stdin
/stdout
-
Extracted the common functions to create the interface.
- Ex. A
DigitalInput
peripheral should have a methodbool Read(void)
.
- Ex. A
-
Implemented the peripheral interfaces using ploymorphism.
C++ provides two mechanisms for polymorphism:
- Object oriented inheritance (runtime polymprohism using the virtual table)
- C++20
concepts
(compile-time polymorphism using templates)
I really wanted to use concepts
because of their novelty and compile-time determinism. I even compared the assembly code which showed the huge overhead of OOP's virtual table.
class DigitalInput {
public:
virtual bool Read() = 0;
};
class stm::DigitalInput {
public:
bool Read() override {
return PLACEHOLDER_PORT & (1 << 4);
}
};
#include <concepts>
template <typename T>
concept DigitalInput = requires(T obj) {
{ obj.Read() } -> std::same_as<bool>;
};
class stm::DigitalInput {
public:
bool Read() { // no override - there is no parent class
return PLACEHOLDER_PORT & (1 << 4);
}
};
The corresponding assembly has 9 overhead instructions (highlighted) to resolve the vtable.
_Z3Readv:
push {r4, lr} ; push registers to stack
ldr r3, .L6 ; load from L6 - holds LA0
ldr r0, [r3] ; load from LA0 - hold addr stm::DigitalInput + 8
ldr r3, [40] ; load from DigitalInput + 8
; holds address of address of stm::DigitalInput::Read
; function (via virtual function table)
ldr r3, [r3] ; load address of stm::DigitalInput::Read
mov lr, pc ; save program counter to link register
bx r3 ; branch to stm::DigitalInput::Read
_ZN4stm12DitialInput4ReadEV: ; pasted here for visualization
ldr r3, .L2 ; load address of IO port
ldr r0, [r3] ; load value of IO port
lsr r0, r0, #4 ; right shift 4 times
and r0, r0, #1 ; mask with 0x01
bx lr ; return to Read
pop {r4, l4} ; restore stack
bx lr ; return
With concepts
, polymorphism is resolved at compile time, so the stm::Read()
function can be executed directly with no overhead!
_Z3Readv:
ldr r3, .L2 ; load address of IO port
ldr r0, [r3] ; load value of IO port
lsr r0, r0, #4 ; right shift 4 times
and r0, r0, #1 ; mask with 0x01
bx lr ; return
I started using concepts for the peripheral interfaces, but its reliance on the template type system meant that every class or function using a peripheral also had to be templated. This made it difficult to write and understand.
Ultimately, using OOP inheritance helped me develop very quickly, so I sacrificed the runtime performace to meet other development deadlines. I am still looking at methods to achieve compile time polymorphism.
Cross Platform Build System
2023, MAC Formula Electric FSAE
Skills: CMake, Bash, Python
When I joined MAC Formula Electric, each ECU (electronic control unit) had its own firmware repository, with its own build configuration, documentation, utility functions, and IDE setup. There was a high barrier to entry for development and no way to share code between projects.
I moved all of these repositories to a monorepo, normalized their folders structures, wrote shared libraries, and create a CMake build system to easily compile any project.
Outcomes
-
Simplicity: All ECU firmware projects can be compiled with a single command, regardless of platform:
make PROJECT=<project-name> PLATFORM=<platform-name>
This produces a binary file when
PLATFORM=stm32
, an executable whenPLATFORM=cli
, etc.The Makefile starts the CMake system and provides other utilities like build cleaning.
-
Shared Libraries: We can share C++ libaries between projects, reducing code duplication and improving development speed.
- IDE Flexibility: We are no longer tied to any specific IDE (like STM32CubeIDE). This means we can use open source editors (like VSCode) and extensions to enhance our development.
- Extensible: We can easily add more features to the build system. For example, we autogenerate C++ code for our CAN messages during the build process.
Firmware Documentation Website
2024 - Present, MAC Formula Electric FSAE
Skills: Markdown, GitHub, Python
See the live website! https://macformula.github.io/racecar/
There was very little documentation when I started leading the firmware & software teams at MAC Formula Electric. I created a documentation site to address the need for a common knowledge resource.
The content is written in Markdown using the Material for MkDocs framework. I chose Markdown to eliminate the learning curve and let me focus on writing content rather than code. Some repetitive content (like the glossary) is generated using Python & Jinja.
The site is hosted through GitHub pages and is connected to our racecar/
repository.
My contributions
- Setting up the website framework and structure.
- Writing articles to help team members learn how to program, compile and flash our firmware.
Some of the articles that I wrote:
Outcomes
- Team members have followed the documentation and started developing without needing assistance.
- Other team members are contributing to the documentation, showing that this framework is easy to use and can be maintained without me.
Examples of member contributions and corresponding pull requests:
Coding Playground
2024 - Present, Personal Project
Skills: Rust, C++
Link to repository: https://github.com/BlakeFreer/Playground
I created the Playground repo to help me prototype, practice, and share small coding projects in different languages.
Some of my favourite projects are:
World's Worst Super Mario Game
https://github.com/BlakeFreer/Playground/tree/main/rust/mario_webserver
Mario's health is encoded in the Rust type system. A TCP web server is used to allow Mario to collect items and be damaged.
The purpose of this project was to familiarize myself with Rust's enums, match
statement, and error handling while learning about TCP servers.

Control Theory Library
https://github.com/BlakeFreer/Playground/tree/main/cpp/control
Implementations of control theory algorithms (like the Kalman filter and Linear Least Squares) as well as example code. I am currently studying "Predictive and Intelligent Control" MECHTRON 4AX3 and this project gives me a way to visualize and verify my understanding.
Uses the C++ Eigen linear algebra library and a CMake build system.

Painter's Grip Pro
Spring 2021, Top First Year Engineering Project
Skills: C++, Arduino, Python, 3D Printing
A smart assistive paintbrush to alert our client of an oncoming fibromyalgia muscle flareup. Detects hand tremors using an accelerometer. Provides visual and auditory feedback when tremors are detected.
My contributions
- Designed and wired the electrical system.
- Programmed an ATmega32 Arduino in C++ to read accelerometer data over I2C, stream data over Bluetooth, and use GPIO to alert the user.
- CAD (Autodesk Inventor) for the paintbrush enclosure and 3D printing it.
Outcome
Selected as one of the Top 3 projects in our first year engineering course ENGINEER 1P13.
McMaster Press Release: https://www.eng.mcmaster.ca/news/first-year-engineering-students-and-budding-designers-showcase-projects-at-year-end-event/
Video Demonstration
All of the Lights
DeltaHacks 8 (2022), Best Hardware Hack
Skills: C++, Arduino, Raspberry Pi, Electronics
For a more in depth description, see the Devpost Link https://devpost.com/software/all-of-the-lights-b31saz
My Contributions
- Built an LED driver using MOSFETs and a step-up logic converter to send a 3.3V UART message to a 5V device.
- Programmed an ATtiny84 in C++ to receive colour sequences over UART and interpolate between colours with PWM signals.
Outcome
Awarded the Best Hardware Hack at DeltaHacks 8.
Video Demonstration
Sumobot
McMaster Sumobot 2021, Best Hardware Design
Skills: C++, Arduino, 3D Printing, Electronics
I joined McMaster Sumobot as way to challenge myself and get involved in the McMaster Engineering community. With my first year of university being online, Sumobots allowed met to get hands-on engineering experience from my own home.
Assembly & Strategy
I assembled the circuit inside my enclosure. I used heatshrink to bundle wires in order to fit everything inside.
My strategy was to search for objects (i.e. enemy robots) and drive straight at them. I used the fact that the forward-facing ultrasonic sensors had different viewing regions. If both sensors detected an object (purple), the robot drove straight. If the object was only visible by one sensor (red or blue), the robot turned in that direction until it was straight ahead. This simple strategy was fairly effective at following moving targets.
If no targets were visible, the robot drove straight until the light sensors detected the edge of the arena. It would turn around and continue searching.
Outcome
Awarded best hardware design in the 2021 McMaster Sumobot Junior competition.
Chassis
After researching Sumobot designs, I decided to stick with the tried and true plow design. I wanted rear wheel drive, as well as ultrasonic sensors to track the opposing robot.
At this point, I had not considered the interior layout of the robot. I quickly realized that the maximum 10 x 10cm footprint is VERY small and I would have to get creative with my space usage.
This is a top-down layout sketch of the interior components of the robot (right edge is the front). Even when only the 3 ultrasonic sensors and 2 motor-wheel assemblies are included, it is already very crowded. The front 2 ultrasonic sensors are recessed from the front edge because of the inwards-sloping ramp.
Since this is a 3D problem, I decided to begin using Autodesk Inventor.
Design 1
For my first design, I focused on being compact (this was a mistake).
I placed the 2 battery packs on the floor at the very front and bottom to weigh down the front edge. The batteries would be inserted from the side.
The 3 ultrasonic sensors are placed just above the batteries, and the Arduino sits horizontally in the middle.
This design has several issues:
-
I planned on 3D printing my chassis, and this would make it difficult to print the roofs above the batteries.
-
I forgot to include space for the breadboard and motor drivers.
-
The shell profile (purple curve in the back) is nearly a cube and has a very small ramp, which is the most important offensive characteristic of the chassis design.
Design 2
After having many troubles with space management in my first design, I restarted and took a different approach. Instead of placing all of the components and trying to fit a shell around it, I designed out the shell first and fit the components into it. Surprisingly, this greatly helped me understand the space involved.
Like the previous design, this one has wheels at the rear and the 9V battery at the front, under the ramp where no other components fit.
Notice the 9V battery and QRD light sensor underneath the ramp. After placing the motors and 9V, I realized that nearly all of the floor space was occupied. This led me to use the vertical space.
The Arduino and Breadboard are located in the middle of the robot. They slide down into channels to hold them upright. They were raised above the floor to allow wires to pass underneath.
The back of the robot is very busy, even with only the motors placed in. I was trying to find space for the 4xAA battery pack when I noticed the empty space above the motors and behind the Arduino.
I built a shelf to support the 4xAA pack and the rear ultrasonic sensor. Since a 3D printer would not be able to make such a large bridge, this part will be printed separately.
I was very satisfied with my design and decided to proceed with it.
After the 3D printer had been running for 6 hours, I noticed that the 9V battery was completely trapped by inner structure of the chassis. I hoped it could fit through the space beneath the ultrasonic sensors (outlined in orange) but the gap was far too tight.
I reluctantly cancelled the print and went back to the drawing board.
9V Battery Holder
To continue using the space under the ramp for the battery, I needed to insert it from a different direction. I chose to insert it through the floor. This means that the battery has to be held against gravity. I experimented with various compliant clip designs and found these to be promising.
Notice the black clip left of the "E" in Energizer, and the pocket left of the clip that provides clearance for the clip to bend.
I test printed the battery holder on its own to save filament, but the exact same clip is embedded into the chassis (right).
Electrical
The 9V provides power to the Arduino and sensors. Despite having a greater voltage, it is not well suited to providing the high current demanded by the motors.
The 4xAA pack provides 6V to the motors at the higher current.
There are 6 total sensors on the robot:
- Two ultrasonic sensors facing forwards to enable primitive object tracking.
- One ultrasonic sensor on the rear to avoid sneak-attacks.
- 2 QRD light sensors to detect the arena boundary.
- One momentary button to start the program (not shown)
RED - Positive | BLACK - Negative | GREEN - Digital Output | BLUE - Digital PWM Output | ORANGE - Digital Input | YELLOW - Analog Input
Serial Communication with LED
May 2021, Personal Project
Skills: Electronics, Arduino, C++
Sending data between Arduinos with an LED and photoresistor.
The LED flashes on and off, causing a voltage signal on the photoresistor. This can be used to transmit data. I achieved perfect transmission at a baud rate of 333 (three milliseconds per bit), above which the photoresistor response time caused errors.
I learned a lot about serial communication and the importance of timing.
See the full project at https://github.com/BlakeFreer/LED-Serial.
FET H-Bridge PCB
Spring 2019, Personal Project
Skills: Electronics, Arduino
My grade 11 shop teacher challenged me to create an H-Bridge motor driver for a "Useless Machine." I had not yet learned about transistors so this was a difficult challenge that took me a couple months and numerous prototypes.
I also built an off-board Arduino, i.e. an ATmega328 with an external oscillator and power supply.
- Both PCBs were designed in AutoCAD.
- I printed the mirrored designs with a laser printer on inkjet paper.
- Used the toner-transfer method to cover my traces on a bare copper board.
- Finally, I soaked the boards in acid to remove the unwanted copper and soldered the components.
Design Process
My shop didn't have any FETs when I started the project, so I began prototyping with BJTs, knowing that the circuit topology would be similar either way.
An H-Bridge works by sending current through a motor in either direction. The FWD and BWD inputs activate either pair of diagonally-opposed transistors.
I initially tried using NPN transistors everywhere, but had issues activating the high side. It wasn't until I studied microelectronics in university that I learned why: the current through an NPN transistor is proportional to the Base-Emitter current, and the B-E current is caused by a voltage gradient between the base and emitter. When used on the high side, the emitter was connected to the high side of the load (i.e. the motor). This voltage is quite close to the supply voltage, so my input signal could not exceed it enough to activate the transistor.
Switching to a PNP transistor avoids this problem, but it must be activated by a low voltage, so I added an NPN inverter to flip the input signal. I learned that I would also need P-type FETs for my real H-Bridge, so I added them to the order.
Once the FETs arrived, I sketched their pinout and made various prototype circuits to learn their behaviour. I learned about pullup and pulldown resistors to prevent floating gates. I combined a N-channel low side driver and P-channel high side driver to make a half H-bridge, then added a second to complete the full H-Bridge. I still used NPN transistors to invert the gate voltage on the P-channel transistors.
I added flyback diodes to the final circuit to protect against voltage spikes when the motor started and stopped, as well as a large capacitor to stabilize the voltage supply. With the circuit complete, I routed the traces in AutoCAD and used fabricated the PCB as described earlier.
Extra Project: This portfolio site!
Thanks for checking out my projects! I hope you enjoyed reading them as much as I enjoyed working on them.
Since you made it this far, you may be interested in how I made this website. I chose Material for MkDocs as my framework. I'm an embedded developer, not a web developer, so I don't care to write a complex website. I'd rather be able to quickly add a new project or article without fighting syntax.
In the spirit of making the development easier, I abstracted out the project details from the this page's Markdown file. I used a json
file to describe each project and used Jinja templating to create all of the cards at the top of the page.
{
"name": "FET H-Bridge PCB",
"description": "Designed an H-Bridge circuit for a DC motor. Used toner-transfer and acid-etching to make my PCB.",
"icon": ":material-electric-switch:",
"date": "Spring 2019",
"note": "Personal Project",
"skills": [
"Electronics",
"Arduino"
],
"writeup": "h_bridge.md"
}
-
FET H-Bridge PCB
Spring 2019, Personal Project
Designed an H-Bridge circuit for a DC motor. Used toner-transfer and acid-etching to make my PCB.
The json
file has one of these entries for each project, as well as a mapping between skills and icons. With all of the project details abstracted out, I can easily change the formatting for all cards simultaneously, and adding a new project is as simple as filling out a new json
block.