WS281x driver for STM Core w/NUCLEO-F030R8

The official STMicroelectronics Arduino core
Post Reply
User avatar
Rick Kimball
Posts: 1038
Joined: Tue Apr 28, 2015 1:26 am
Location: Eastern NC, US
Contact:

WS281x driver for STM Core w/NUCLEO-F030R8

Post by Rick Kimball » Thu Jun 22, 2017 5:03 pm

I took a novel approach to driving a string of 5 WS281x pixels. I configured a TIMER to drive a PWM output at 750kHz and physically connected that output to the SPI SCK. Then I configured the SPI device as a slave and use simple HAL calls to send the pulses. The WS281x pulse waves are produced on the MISO pin. No DMA, no weird timing glitches. The TIMER produces a jitter free clock for the SPI.

This code should probably go in the code snippet forum. However, this is very specific to the STM Core and uses HAL functions to configure the TIMER and SPI, so I didn't want to confuse people who are using libmaple. Someone could probably do the same type thing with the libmaple.

This code works for the NUCLEO-F030R8 board:

Code: Select all

/*
   ws281x driver code for NUCLEO=F030R8

   for use with: https://github.com/stm32duino/Arduino_Core_STM32

   This code lights up 5 led pixels on a WS281x strip.  It configures the SPI as
   a slave and drives the SPI1_SCK using TIM3 CH2 at 750kHz

   This provides a uninterrupted SPI stream and makes it easy to send out
   WS281x conforming pulse waves.without any jitter.

   D12 should be wired to the DIN pin of the WS281x pixel
   D9 should be wired to the D13 (SCK)
   GND should be connected between the NUCLEO and the WS281x GND
   5V from the NUCLEO supplies 5V to the WS281x

   the 6MHz clock provides a period of 8/6000000 or 1.333 useconds
   the ZERO pulse is 333.333 ns
   the ONE pulse is 666.666 ns
*/


#define PULSE0 0b11000000  /* 2 bits = 2/6000000 = 333.333 ns */
#define PULSE1 0b11110000  /* 4 bits = 4/6000000 = 666.666 ns */

#define COLORx00 PULSE0, PULSE0, PULSE0, PULSE0, PULSE0, PULSE0, PULSE0, PULSE0
#define COLORx01 PULSE0, PULSE0, PULSE0, PULSE0, PULSE0, PULSE0, PULSE0, PULSE1
#define COLORx03 PULSE0, PULSE0, PULSE0, PULSE0, PULSE0, PULSE0, PULSE1, PULSE1
#define COLORx07 PULSE0, PULSE0, PULSE0, PULSE0, PULSE0, PULSE1, PULSE1, PULSE1
#define COLORx0F PULSE0, PULSE0, PULSE0, PULSE0, PULSE1, PULSE1, PULSE1, PULSE1
#define COLORx1F PULSE0, PULSE0, PULSE0, PULSE1, PULSE1, PULSE1, PULSE1, PULSE1
#define COLORx3F PULSE0, PULSE0, PULSE1, PULSE1, PULSE1, PULSE1, PULSE1, PULSE1
#define COLORx7F PULSE0, PULSE1, PULSE1, PULSE1, PULSE1, PULSE1, PULSE1, PULSE1
#define COLORxFF PULSE1, PULSE1, PULSE1, PULSE1, PULSE1, PULSE1, PULSE1, PULSE1
#define COLORRST      0,      0,      0,      0,      0,      0,      0,      0
#define COLOR_RST COLORRST, COLORRST, COLORRST

#define RGB(r,g,b) g,r,b

#define RED    RGB(COLORx1F, COLORx00, COLORx00)
#define GREEN  RGB(COLORx00, COLORx1F, COLORx00)
#define BLUE   RGB(COLORx00, COLORx00, COLORx1F)
#define YELLOW RGB(COLORx1F, COLORx1F, COLORx00)
#define WHITE  RGB(COLORx1F, COLORx1F, COLORx1F)
#define BLACK  RGB(COLORx00, COLORx00, COLORx00)

#define NUM_LEDS 5
#define NUM_LED_COLORS 3
#define NUM_LED_BYTES_PER_COLOR_BIT 8
#define BYTES_PER_FRAME ((NUM_LEDS+2)*3*8)
// the NUM_LEDS+2 extra entries end up as zeros
// this allow the TIM3_CH2 to run continuously at
// ~750k or (1/6MHZ) when we aren't sending
// any bytes it keeps the MISO pin low


// ----------------------------------------------------
// each frame sets the 5 pixels to a new color
// to set each pixel requires 8 pulses of bits
// for Green, Red, and BLUE.  So a total of 24
// bytes of pulses per pixel.
// The color_frames static array is just to make a simple
// animation to produce a pattern and illustrate the
// techinque of driving the SPI slave just using the
// HAL_SPI_Transmit() function.
//
// NOTE: there must be an extra pixel that is all zero's
// in the array below this is handled implicitly by
// creating the array with an extra byte for each frame
// the default value is 0 so that drives the SPI low
// after we send all our pixel bytes.
//
uint8_t color_frames[][BYTES_PER_FRAME] = {
  { WHITE, RED,   GREEN, BLUE,  BLACK },
  { BLACK, WHITE, RED,   GREEN, BLUE  },
  { BLACK, BLACK, WHITE, RED,   GREEN },
  { BLACK, BLACK, BLACK, WHITE, RED   },
  { BLACK, BLACK, BLACK, BLACK, WHITE },
  { BLACK, BLACK, BLACK, BLACK, BLACK },
  { WHITE, WHITE, WHITE, WHITE, WHITE },
  { BLACK, BLACK, BLACK, BLACK, BLACK },
  { WHITE, WHITE, WHITE, WHITE, WHITE },
  { BLACK, BLACK, BLACK, BLACK, BLACK },
};

uint32_t frame_delay[] = {
  500, 500, 500, 500, 500, 500, 250, 250, 250, 250
};

SPI_HandleTypeDef hspi1;
TIM_HandleTypeDef htim3;

void setup() {
  GPIO_Init();
  SPI1_Init();
  TIM3_Init();

  HAL_RCC_MCOConfig(RCC_MCO, RCC_MCO1SOURCE_SYSCLK, RCC_MCODIV_1);

  __HAL_TIM_ENABLE(&htim3);
  HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);
}

void loop() {

  while (1)  {
    int x = 0;

    do {
      HAL_SPI_Transmit(&hspi1, &color_frames[x][0], BYTES_PER_FRAME, 40);
      delay(frame_delay[x]);
    } while ( ++x < sizeof(color_frames) / sizeof(color_frames[0]) );
  }
}


// --------------------------------------------------------------------
// HAL STUFF - not very arduino but easy to generate from STM32CubeMX
// --------------------------------------------------------------------

void _Error_Handler(char *f, int)
{
  while (1) {
    __asm("nop");
  }
}

/*
   configure SPI as a slave
*/
static void SPI1_Init(void)
{
  hspi1.Instance = SPI1;
  hspi1.Init.Mode = SPI_MODE_SLAVE;
  hspi1.Init.Direction = SPI_DIRECTION_2LINES;
  hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
  hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; GPIO_Init
  hspi1.Init.CLKPhase = SPI_PHASE_2EDGE;
  hspi1.Init.NSS = SPI_NSS_SOFT;
  hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
  hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
  hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
  hspi1.Init.CRCPolynomial = 7;
  hspi1.Init.CRCLength = SPI_CRC_LENGTH_DATASIZE;
  hspi1.Init.NSSPMode = SPI_NSS_PULSE_DISABLE;
  if (HAL_SPI_Init(&hspi1) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

  /* USER CODE END SPI1_MspInit 0 */
  /* Peripheral clock enable */
  __HAL_RCC_SPI1_CLK_ENABLE();

  GPIO_InitTypeDef GPIO_InitStruct;
  /**SPI1 GPIO Configuration
    PA5     ------> SPI1_SCK
    PA6     ------> SPI1_MISO
  */
  GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_6;
  GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
  GPIO_InitStruct.Pull = GPIO_PULLDOWN;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
  GPIO_InitStruct.Alternate = GPIO_AF0_SPI1;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}


/*
   configure TIM3 channel 2 as a PWM 750kHz signal
*/
static void TIM3_Init(void)
{

  TIM_ClockConfigTypeDef sClockSourceConfig;
  TIM_MasterConfigTypeDef sMasterConfig;
  TIM_OC_InitTypeDef sConfigOC;

  htim3.Instance = TIM3;
  htim3.Init.Prescaler = (SystemCoreClock / (6000000 * 2)) - 1;
  htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim3.Init.Period = 2 - 1;
  htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  if (HAL_TIM_Base_Init(&htim3) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  if (HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

  if (HAL_TIM_PWM_Init(&htim3) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

  sConfigOC.OCMode = TIM_OCMODE_PWM1;
  sConfigOC.Pulse = 1;
  sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
  sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
  if (HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_2) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

  GPIO_InitTypeDef GPIO_InitStruct;

  if (htim3.Instance == TIM3)  {
    /**TIM3 GPIO Configuration
      PC7     ------> TIM3_CH2
    */
    GPIO_InitStruct.Pin = GPIO_PIN_7;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_PULLDOWN;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF0_MCO;
    HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
  }
}

static void GPIO_Init(void)
{
  __HAL_RCC_GPIOF_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOC_CLK_ENABLE();
}
-enjoy
-rick

User avatar
RogerClark
Posts: 7147
Joined: Mon Apr 27, 2015 10:36 am
Location: Melbourne, Australia
Contact:

Re: WS281x driver for STM Core w/NUCLEO-F030R8

Post by RogerClark » Thu Jun 22, 2017 10:47 pm

Rick

Does this snippet allow generation of any 24 bit RGB colour?

I can see how the transmission code could transmit any bit sequence, but I cant understand how the code builds the data for a 24 bit value.
It seems only have macros for a limited palette of colours

User avatar
Rick Kimball
Posts: 1038
Joined: Tue Apr 28, 2015 1:26 am
Location: Eastern NC, US
Contact:

Re: WS281x driver for STM Core w/NUCLEO-F030R8

Post by Rick Kimball » Thu Jun 22, 2017 11:40 pm

Sure you can generate any 24 bit value but my example is simplistic on purpose. I probably could have written a routine to take a 32 bit RGB value and generate 24 bytes of bit pulses ... but I didn't. This is more to show how to use the underlying SPI + TIMER + HAL SPI call as a slave without flogging yourself.
-rick

User avatar
RogerClark
Posts: 7147
Joined: Mon Apr 27, 2015 10:36 am
Location: Melbourne, Australia
Contact:

Re: WS281x driver for STM Core w/NUCLEO-F030R8

Post by RogerClark » Fri Jun 23, 2017 12:43 am

Rick Kimball wrote:
Thu Jun 22, 2017 11:40 pm
Sure you can generate any 24 bit value but my example is simplistic on purpose. I probably could have written a routine to take a 32 bit RGB value and generate 24 bytes of bit pulses ... but I didn't. This is more to show how to use the underlying SPI + TIMER + HAL SPI call as a slave without flogging yourself.
No worries

I thought perhaps I didn't understand what the code was doing.

Post Reply