brucehoult, a great example of custom tooling. This mindset is very powerful and flexible; once you are at it, nothing prevents you doing the same on SPI, I2C, or even bitbanging on an GPIO if that is all you have, relevant on small microcontrollers. And works a treat during runtime, without special HW, through the interfaces you would use anyway.
As no one requires you to follow any existing protocol, the design can be so simple it's not a big deal to implement; just change it on the fly.
My current project involves a Cortex M7 microcontroller streaming a lot of pre-processed sensor data to a host computer, using SPI at 25 MHz. For combining flexibility and ability to debug, the very first thing I wrote was a simplistic subscription model, where the host computer can subscribe to certain data source feeds dynamically. When this SPI packetizer is abstracted in its own module, it looks extremely simple for all other modules: you have a struct for each data type, and a pointer, which, when non-NULL, gets you to the correct place in the output FIFO buffer. So if I want to debug a, say, inertial measurement unit (IMU), the IMU code simply does:
if(p_message_imu) // subscription enabled
{
p_message_imu->latest_gyro = cur_gyro;
p_message_imu->latest_acceleration = cur_acceleration;
}
And the parser on the host side automagically calls a function pointer, which does whatever processing is needed during production, or calls printf, or graphical UI draw functions, or whatever needed for fancy output; the same headers are used everywhere.
Things like these are surprisingly easy when you are in the mindset of creating your tooling.