Bit-bang SPI on STM32 HAL
As I wrote in a previous article, I am trying to write my own BLDC firmware. I am trying to save time by using the ST motor control SDK, but it is still not documented well enough for my taste, forcing me to dig into the generated source code. It is sufficiently complex that lots of trial and error is required to build understanding. The ST MC SDK has a very nice feature to write out a couple of quantities of interest to the 2 DAC channels available (on the MCU that has the DAC hardware). But this is not enough for me, and besides, event logging with flexible number of arguments is the right answer. In the past, I've used 16-bit SPI peripheral and DMA available on the STM32 MCU to burst a few words of the event log out of the MCU to a logic analyzer already probing the CLK and MOSI pins of the MCU. But on the off-the-shelf ESC (electric speed controller) I am planning to use for my RC car does not have free pins that can be attached to the STM32 SPI peripheral. [On many MCU, a peripheral may be assigned to only some pre-defined pins.] So in this article, I explore how to bit-bang a SPI half-duplex writer with just the MCU code controlling 2 GPIO output pins.
After creating a ST MC workbench project for my eval board pair (nucleo-f446re hosting the 3 phase inverter board x-nucleo-ihm08m1), "Generate Code" button in the ST MC workbench produces the ST CubeMX project (and also runs it to generate the ST Cube IDE project from it, since that is the IDE I have chosen to use). The pins assigned to the motor control functions can be enumerated within the MC workbench, but the CubeMX can render the pin assignment. I picked 2 available pins on the nucleo-f446re board for GPIO output and named them appropriately, as shown here.
To ensure that the GPIO pin can keep up with the fast toggling, I increased the output speed in the GPIO configuration, as shown here. According to Mastering STM32 by Carmine Noviello, the slew rate increase from the low speed to very high is about 2x (40 ns to 20 ns). From the CubeMX clock tree view, it looks like the APB bus, where the GPIO peripheral hangs, is running at 48 MHz, so a slight boost in the toggle rate looks like it might buys us some timing margin when trying to drive the SPI clock as fast as the APB bus will allow.
After generating the code from the CubeMX and opening the Cube IDE project, I see that the 2 GPIO pins are initialized in MX_GPIO_Init() called from the main(). To visualize the current control loop duty cycle, I also enabled another GPIO pin, to indicate when the high/medium priority motor control task runs. I can then supply a function to bit-bang the MOSI action SPI mode 0 (which means polarity 0, phase 0).
void BBSpiMode0_write_only(uint32_t wb) { uint32_t msk = 1UL << 31; do { BB_SPI_CLK_GPIO_Port->ODR &= ~BB_SPI_CLK_Pin; // deassert CLK if(wb & msk) { // set MOSI BB_SPI_MOSI_GPIO_Port->ODR |= BB_SPI_MOSI_Pin; } else { BB_SPI_MOSI_GPIO_Port->ODR &= ~BB_SPI_MOSI_Pin; } BB_SPI_CLK_GPIO_Port->ODR |= BB_SPI_CLK_Pin; // assert CLK // The slave will read the MOSI pin during the transition } while(msk>>=1); BB_SPI_CLK_GPIO_Port->ODR &= ~BB_SPI_CLK_Pin; // deassert CLK }
Using this underlying primitive to push out 32 bit at a time, I can log variable length arguments.
struct LogItem { uint16_t loc; uint8_t module; uint8_t n; uint32_t arg[4]; } __attribute__((aligned(4))); void spilog(uint8_t module, uint16_t loc, const char* fmt, ...) { struct LogItem log; uint8_t n = 0; log.module = module; log.loc = loc; if (fmt && *fmt) { union Accum { uint32_t dw; uint16_t uw[2]; int16_t sw[2]; } tmp = { .dw = 0 }; bool even = false; va_list args; va_start(args, fmt); for ( ; *fmt && n < Q_DIM(log.arg); ++fmt) { switch (*fmt) { case 'h': // short tmp.sw[even] = (int16_t)va_arg(args, int); even = !even; if (!even) log.arg[n++] = tmp.dw; break; case '2': // unsigned short (should be hu but I got lazy) tmp.uw[even] = (uint16_t)va_arg(args, int); even = !even; if (!even) log.arg[n++] = tmp.dw; break; case 'd': *((int32_t*)&log.arg[n++]) = va_arg(args, int); break; case '4': log.arg[n++] = va_arg(args, unsigned); break; default: break; } } va_end(args); } log.n = n; // write out the MOSI now const uint32_t* p = (const uint32_t*)&log; BBSpiMode0_write_only(*p); // write header for (unsigned i=0; i < log.n; ++i) { //write arguments BBSpiMode0_write_only(*++p); } }
The algorithm can then log its action with convenience macro such as this.
#define Q_DEFINE_THIS_MODULE(name_) static uint8_t const Q_this_module_ = name_; void spilog(uint8_t file, uint16_t loc, const char* fmt, ...); // @precondition a static Module declaration is necessary for all callers // @note ## is necessary to support 0 argument #define SPI_LOG(fmt_, ...) spilog(Q_this_module_, __LINE__, fmt_, ##__VA_ARGS__) // @brief Just a "I am here" message // @precondition a static File declaration is necessary in all files that calls SPI_LOG #define SPI_TRACE() spilog(Q_this_module_, __LINE__, NULL)
An example usage in the motor controller FSM (state_machine.c:STM_NextState):
SPI_LOG("22", bNewState, bState);
The state transitions can then be visualized in the logic analyzer. Note the SPI protocol analyzer displays the log's file/line location, and arguments.