Writing the Native Function

You may skip this step if you simply want to download the pre-compiled firmware files I've provided in the download page.
In the previous step, I said I added "NeoPixel_NativeCode.cpp" and "NeoPixel_NativeCode.h". We now have to write these files. The CPP file will contain the actual function and private definitions while the H file will contain the public function pointer and public definitions.

I avoided writing too many things into the auto-generated stub files, but I did make the stub function call my own function.

The function to send data to the NeoPixels is very simple.

  1. The first thing it does is translate Netduino's pin assignment from an integer to the specific hardware output port and bit. (this was sort of confusing to do, but I got some clues after reading the file "STM32_GPIO_functions.cpp")
  2. Then it disables all interrupts so it can output pulses with correct uninterrupted timing.
  3. Then it iterates through all the bits in all the bytes, inserting the correct (read the WS2813 datasheet) amount of delay using delay loops (a for-loop macro made with an volatile down-counting integer and a NOP instruction)
  4. Finally, it re-enables all interrupts
#include <TinyCLR_Interop.h>

#if defined(PLATFORM_ARM_STM32F4_ANY)
#include <DeviceCode/stm32f4xx.h>
#elif defined(PLATFORM_ARM_STM32F2_ANY)
#include <DeviceCode/stm32f2xx.h>
#else
#include <DeviceCode/stm32f10x.h>
#endif

#include "NeoPixel.h"
#include "NeoPixel_NeoPixel_NeoPixelNative.h"
#include "NeoPixel_NativeCode.h"

#define NP_DELAY_LOOP(x) do { volatile int ___i = (x); while (___i--) { asm volatile ("nop"); } } while (0) // every loop is 5 ticks of CPU clock (using GCC with full optimization)

// these numbers were tuned specifically for Netduino Plus 2, STM32F4 running at 168MHz
// tuning done using an o'scope
#define NP_WAIT_T1H() NP_DELAY_LOOP(12) // 700 ns needed, tested 12 = 680ns
#define NP_WAIT_T1L() NP_DELAY_LOOP(10) // 600 ns needed, tested 10 = 580ns
#define NP_WAIT_T0H() NP_DELAY_LOOP(5)  // 350 ns needed, tested 5  = 350ns
#define NP_WAIT_T0L() NP_DELAY_LOOP(15) // 800 ns needed, tested 15 = 800ns

void NeoPixelNativeWrite(CLR_RT_TypedArray_UINT8 data, INT32 count, UINT32 pin)
{
	GPIO_TypeDef* port = ((GPIO_TypeDef *) (GPIOA_BASE + ((pin & 0xF0) << 6)));
	UINT16 *_BSRRH, *_BSRRL;
	// note: I think the BSRRx registers are reversed in the declaration, hence why this code seems reversed
	#if defined(PLATFORM_ARM_STM32F2_ANY) || defined(PLATFORM_ARM_STM32F4_ANY)
	_BSRRH = (UINT16*)&(port->BSRRL); _BSRRL = (UINT16*)&(port->BSRRH);
	#else
	_BSRRH = (UINT16*)&(((UINT16*)(&port->BSRR))[1]);  _BSRRL = (UINT16*)&(((UINT16*)(&port->BSRR))[0]);
	#endif
	UINT8 portBit = 1 << (pin & 0x0F);
	*_BSRRL = portBit; // clears the pin low

	__disable_irq(); // disable interrupts so that timing is accurate

	// pin is already assumed low
	// data is already in GRB order
	INT32 byteIdx, bitMask;
	INT32 cx3 = count * 3; // 3 bytes per neopixel
	for (byteIdx = 0; byteIdx < cx3; byteIdx++) // for every byte in the array
	{
		for (bitMask = 0x80; bitMask > 0; bitMask >>= 1) // MSB first
		{
			*_BSRRH = portBit; // sets the pin high
			if ((data[byteIdx] & bitMask) != 0)
			{
				NP_WAIT_T1H();
				*_BSRRL = portBit; // clears the pin low
				NP_WAIT_T1L();
			}
			else
			{
				NP_WAIT_T0H();
				*_BSRRL = portBit; // clears the pin low
				NP_WAIT_T0L();
			}
		}
	}

	__enable_irq();
}
Please download the files to see them all (all project downloads are in another page, see the navigation panel)

If everything goes right, you should be able to build and update the firmware, by going through the same steps I've already shown you.

Testing and Verification


OK! I've done the grunt work for you (It took several trial-and-error tests to get right!) While working on it, I noticed these things:
  • the "BSRRL" and "BSRRH" registers are apparently role-reversed in the file that defines hardware peripheral registers. I had to swap them inside my code.
  • the overhead of the CLR is so long that I didn't need to add the 50 microsecond delay required to latch all the NeoPixels
I also had to tune the delay loop counts manually, which is common when dealing with these delicate bit-bang encoding. However, they should work great now - see o'scope screenshot below (it shows binary 1 0 1 0)

Some tips for when you write your own firmware:


Look around the porting kit and Netduino firmware for files that might contain helpful functions and definitions. For example, some timing and delay utilities are found inside "C:\PortingKitPath\DeviceCode\Targets\Native\STM32\DeviceCode\STM32_Time\STM32_time_functions.cpp". STM32F4's hardware definitions are inside "C:\PortingKitPath\DeviceCode\Targets\Native\STM32\DeviceCode\system_stm32f4xx.h", and I'm sure there are other useful files.

But you need to know how to include these files, because the paths are so messed up. Here's a list of paths that are included by the compiler (via the "-I" option)
  • c:\PortingKitPath\DeviceCode\pal\Double
  • c:\PortingKitPath\DeviceCode\include
  • c:\PortingKitPath\DeviceCode\Cores\arm
  • c:\PortingKitPath\Support\Include
  • c:\PortingKitPath\crypto\inc
  • c:\PortingKitPath\CLR\Include
  • c:\PortingKitPath\CLR\Libraries\CorLib
  • c:\PortingKitPath\CLR\Libraries\SPOT
  • c:\PortingKitPath\CLR\Libraries\SPOT_Hardware
  • c:\PortingKitPath\CLR\Libraries\SPOT_Graphics
  • c:\PortingKitPath\CLR\Libraries\SPOT_Net
  • c:\PortingKitPath\Solutions\NetduinoPlus2
  • c:\PortingKitPath\devicecode\Targets\Native\STM32
  • c:\PortingKitPath\DeviceCode\Cores\arm
  • c:\PortingKitPath\DeviceCode\Cores\arm\Include
  • c:\PortingKitPath\DeviceCode
  • c:\PortingKitPath\DeviceCode\Include
  • c:\PortingKitPath\CLR\Include
  • c:\PortingKitPath\DeviceCode\Targets\Native\STM32\NetduinoPlus2
  • c:\PortingKitPath\Support\Include
  • c:\PortingKitPath\DeviceCode\include
  • c:\PortingKitPath\CLR\include
  • c:\PortingKitPath\Crypto\inc
  • c:\PortingKitPath\DeviceCode\include
Using " #include <> "relative to any of these paths will allow your file to be found.

Be prepared to open "msbuild.log" a lot! I suggest keeping it open in Notepad++ because every time it changes, Notepad++ will ask you if you want to reload the file, which is faster than reopening it manually.

This build log file is 3 MB in size most of the time! Have fun looking for the errors inside it. The very last part of the log indicates which step it failed, but not why. But if it is an error involving the compiler (gcc) or the linker (ld), then search the build log for
: error:
and you should be able to find what exactly went wrong.

Each full build or rebuild takes 5 minutes, each partial rebuild takes around 1 minute. This is on my computer with a SATA 3 SSD and Intel Core i7 Ivy Bridge. If you are doing this often, get a snack, turn on a TV...
Last updated on May 04, 2015 Published on Sep 12, 2013