The STM32F4xx devices are not short of pins and they can be more flexibly used than on the STM32F1xx types. On a micromouse though, there is not a lot of physical space and I am not good at routing lots of traces around a board. Since I had already decided to add an SPI driven LED dot matrix display, I went hunting for a suitable LED driver and came across the Maxim MAX6966 10 channel LED driver.

This device has 10 ports, each with PWM capable of constant current drive that can be set at one of several maximum levels up to 20mA. It has an SPI interface that I can share with the dot matrix display and so gives me up to 10 indicator LEDs for the cost of only a couple of traces running to the back of the mouse. It seems ideal and, since the ports on it can be configured as either inputs or outputs is well worth a look.

You can find out more about these at the Maxim site here: I wired one of these to a small piece of perfboard and then connected that up to my test setup with an STM32F4Discovery. This is where all the peripheral development will be done for the micromouse so I have connected up buttons, displays, sensors and motor drivers so that I can write the basic drivers to be sure that my boards will work properly.

MAX6966 connected to an STM32F4 Discovery for micromouse development

Although the ports on the MAX6966 can be used as inputs, I decided to use them for output only so relieving me of the need to add an MISO line to the SPI port connection. Now, I only need the Chip Select, MOSI and SCK connections to drive up to 10 LEDs. Originally, it had been my hope to use DMA to transfer all 10 LED settings at once but a careful read of the datasheet showed that the CS line had to be brought high after each command and it was not worth the overhead of servicing the DMA interrupt for each 16 bit instruction. That meant that all I needed was a relatively simple polling mode driver for the chip.

Although I had written one before for the STM32F103, the ‘F4 has a few subtle changes to the setup and use of just about everything.

First job is to define and setup the various pins for the SPI.

#define SPI_PORT                  SPI2
#define SPI_PORT_CLOCK            RCC_APB1Periph_SPI2
#define SPI_PORT_CLOCK_INIT       RCC_APB1PeriphClockCmd

#define SPI_SCK_PIN              GPIO_Pin_13
#define SPI_SCK_GPIO_PORT        GPIOB
#define SPI_SCK_GPIO_CLK         RCC_AHB1Periph_GPIOB
#define SPI_SCK_SOURCE           GPIO_PinSource13
#define SPI_SCK_AF               GPIO_AF_SPI2

#define SPI_MOSI_PIN             GPIO_Pin_15
#define SPI_MOSI_GPIO_CLK        RCC_AHB1Periph_GPIOB
#define SPI_MOSI_SOURCE          GPIO_PinSource15
#define SPI_MOSI_AF              GPIO_AF_SPI2

Nothing too special here. I like to define everything about the device as macros in a hardware configuration header file to make it easier to move it to another project later. The SPI port can now be configured as single direction master using polled mode with software slave select. Actually there is nothing in the configuration about polled mode. That is just how it works by default. For DMA you need to do a bunch of different things as well. There will be another post about that later. Note that the MAX6966 uses the more common arrangement of clock polarity and phase:

void HardwareSPI_init(void) {
  SPI_InitTypeDef SPI_InitStructure;
  GPIO_InitTypeDef GPIO_InitStructure;
  // enable the SPI peripheral clock
  // enable the peripheral GPIO port clocks
  // Connect SPI pins to AF5 - see section 3, Table 6 in the device datasheet
  // now configure the pins themselves
  // they are all going to be fast push-pull outputs
  // but the SPI pins use the alternate function
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
  GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
  GPIO_InitStructure.GPIO_Pin = SPI_SCK_PIN;
  GPIO_Init(SPI_SCK_GPIO_PORT, &GPIO_InitStructure);
  GPIO_InitStructure.GPIO_Pin = SPI_MOSI_PIN;
  GPIO_Init(SPI_MOSI_GPIO_PORT, &GPIO_InitStructure);
  // now we can set up the SPI peripheral
  // Assume the target is write only and we look after the chip select ourselves
  // SPI clock rate will be system frequency/4/prescaler
  // so here we will go for 72/4/8 = 2.25MHz
  SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
  SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Tx;
  SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
  SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8;
  SPI_Init(SPI_PORT, &SPI_InitStructure);
  // Enable the SPI port
  spiConfigured = 1;

The spiConfigured global flag makes it easy to simply initialise a device and have it initialise the SPI port if it has not already been done. The most obvious difference between this code and that needed for the STM32F103 chip is the way that Alternate Functions are allocated. There is much more flexibility but the code has to be different.

The MAX6966 itself gets similar treatment to define the hardware:

#define MAX6966_CS_PIN               GPIO_Pin_11
#define MAX6966_CS_GPIO_PORT         GPIOB
#define MAX6966_CS_GPIO_CLK          RCC_AHB1Periph_GPIOB
#define MAX6966_CS                   MAX6966_CS_GPIO_PORT, MAX6966_CS_PIN
#define MAX6966_SELECT()             GPIO_ResetBits(MAX6966_CS)
#define MAX6966_DESELECT()           GPIO_SetBits(MAX6966_CS)
#define MAX6966_IS_SELECTED()        GPIO_ReadOutputDataBit(MAX6966_CS)

Since I am going to be sharing this device with other things on the SPI port, I will be using software defined Slave Select pins and it makes sense to define all that here.

Initialising the MAX6966 is now quite straightforward:

void MAX6966_init(void) {
  GPIO_InitTypeDef GPIO_InitStructure;
  int i;
  if (!spiConfigured) {
  RCC_AHB1PeriphClockCmd(MAX6966_CS_GPIO_CLK, ENABLE);
  // now configure the pins themselves
  // they are all going to be fast push-pull outputs
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
  GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
  GPIO_InitStructure.GPIO_Pin = MAX6966_CS_PIN;
  GPIO_Init(MAX6966_CS_GPIO_PORT, &GPIO_InitStructure);
  MAX6966_putWord(0x1005); // place device in run mode
  MAX6966_putWord(0x0AFF); // all ports as high impedance
  for (i = 0; i < 10; i++) {
    setLED(i, 0); // set each port to active LED but off
  currentLED = 0;

Only the Chip Select line needs actual setting up. The SPI connections are handled in the SPI setup code. A array of 10 bytes hold the current intensity for each LED and a helper function, setLED() allows easy setting of the intensity for each channel. The variable currentLED is used as part of the background task that maintains the display.

Before that, look at how data is sent to the chip. In the chip driver there is a simple function that selects it, write a word out over SPI and then deselects it:

void MAX6966_putWord(uint16_t data) {

The code in the SPI driver assumes that the appropriate Slave Select line has been asserted and can concentrate on just putting data out over the SPI:

void spiPutWord(uint16_t data) {
  // make sure the transmit buffer is free
  while (SPI_I2S_GetFlagStatus(SPI_PORT, SPI_I2S_FLAG_TXE) == RESET);
  SPI_I2S_SendData(SPI2, data / 256);
  // be sure that the character goes to the shift register
  while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET);
  SPI_I2S_SendData(SPI2, data % 256);
  // be sure that the character goes to the shift register
  while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET);
  // and then be sure it has been sent over the wire
  while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_BSY) == SET);

This is pretty stock stuff taken from the Peripheral Library Examples. Whenever you send data over SPI, you must first make sure that the transmit buffer is empty. Data is sent MSB first and, for a 16 bit transfer, the high byte goes first. After the transmit buffer is loaded, one of two things can happen. Either the shift register is empty, in which case the transmit buffer is immediately sent to the shift register and becomes available, or the shift register has not finished sending out the last byte so the peripheral has to wait before it can transfer the new contents out of the transmit buffer and make it available. If you don’t wait for the transmit buffer to become available, you will lose data.

After you get the last byte into the transmit register, you must wait until the shift register is empty before returning to the caller. Unless that is done, the caller will probably deselect the target device before all the data has been sent and, once again, you will lose data.

Now we have the chip set up and a way to get data to it. That data consists of a port number and an intensity level. The simplest way to manage this would be to have setLED() write the information directly to the chip. However, I am going to have more than one device on that SPI port and the other device will be using DMA so I cannot be entirely sure that the SPI peripheral will be available whenever I want to send stuff to the MAX6966. My solution is to have all SPI transfers initiated by the 1kHz systick event used by the micromouse.

On each tick, a call is made to MAX6966_update() which send out data for a single LED. That takes only about 8us with the speed I run the processor and SPI so it is not very costly. On each tick, a different LED gets updated so, after only 10ms, all of them have been done. I can update each LED 100 times per second for very little cost in processor time. The DMA transfer for the other device gets started after the MAX6966 is updated so there is no danger of the peripheral being in use for two different devices at the same time.

Here is the code that records the level for an LED:

static uint8_t LEDbrightness[10];
static uint8_t currentLED;

void setLED(uint8_t led, uint8_t value) {
  if (led > 9) return;
  if (value < 3) value = 3;
  if (value > 250) value = 250;
  LEDbrightness[led] = value;

The lower and upper limits on brightness exist because values of 0, 1, 2 and 255 have a special meaning to the device. Have a look at the datasheet for more information.

And this is the code that gets called at each systick:

void MAX6966_update(void) {
  if (currentLED > 9) {
    currentLED = 0;
  MAX6966_putWord((uint16_t) currentLED * 256 + LEDbrightness[currentLED]);

All that doesn’t seem too complicated to me. There are 10 LEDs available (I only use 8 on the micromouse – not sure why now), they can be set to an arbitrary brightness level and they get updated 100 times per second. That’s not too shabby for a couple evenings work and only three traces going to the other end of the mouse – two of which were going there anyway to drive another display.

Here are the complete file listing with all the gory details in one handy zip file:



This Post Has 18 Comments

  1. Ullasmann

    Hi there.

    Nice work indeed 🙂

    Just one question!

    Why do you select the SPI_NSS bit as being Software controlled, when

    you are actually using a seperate Chip_Select on GPIO_Pin_11 to control

    the MAX6966_CS_PIN ? Or am I simply overseeing something?

    Keep up the good work 😉


  2. Peter Harrison

    Thank you.

    Perhaps I don’t understand the question. I have more than one device on this SPI port and so they need separate chip select lines so the hardware NSS option is no good to me.

  3. Ullasmann

    Hi Peter.

    Ok, I simply wasn’t aware, that you were using the same Spi port to drive more than 1 peripheral, mea culpa 🙂

    As an ex-AVR assembler freak, I find the stm32 library sometimes to be almost so abstracted from the bit level, that one can hardly understand the underlying sense of things. But then that probably has to do with me only 🙂

    Just to be certain, Peter, should the SPI control register 1 (SPI_CR1) be so initialised:

    1110 0010 0001 0111 = 0xE217 ?

    I mean if I’ve rightly understood you?



  4. Peter Harrison

    Sorry – I would not have an easy answer to that. Having relied upon the library, working backwards is a bit of hard work. In particular, bit 6 should be set if the peripheral is enabled, the clock and phase bits (0 and 1) will depend upon the target device and bits 3:5 set the speed.

  5. Ullasmann

    Hi Peter.

    It’s working now.

    Many thanks for the insights I obtained on your website.



  6. ivitro

    Hi can you tell me why SPI clock rate will be system frequency/4/prescaler and not system frequency/prescaler?


  7. Peter Harrison

    That looks like a mistake on my part. I will check it when I can.
    The clock rate is the APB1 clock frequency/prescaler.
    I think the default for APB1 is system frequency/2 in the peripheral library setup and I can’t now find where I might have set it to anything else. There is an upper limit for that clock so it would not be safe to use that default if the processor were to be run at faster than 160MHz.

  8. ivitro

    In system_stm32f4xx.c the APB1 prescaler = 2

    So fPCLK1=72/2=36MHz, which mean that the spi will work at 36/8=4,5MHz, is this right?

    I’m trying put the 3310lcd work with spi peripheral in STM32F1 D. and STM32F4 but without success.

  9. Peter Harrison

    I am afraid you will have to give me a few days before I can check this.

  10. ivitro


    I already have STM32F1 running the 3310 lcd with SPI 🙂

    Now I’m working on STM32F4.

    Thanks Ullasmann and Peter.

    ps: @Ullasmann you did the code in assembly :O really nice 😛

  11. ivitro


    Finally I got the discovery board speaking with the lcd3310 by SPI communication.

    I don t have sure but please correct me if I am wrong.

    I have the APB1@48MHz, and the SPI prescaler = 2, that means that SPI is running @29MHz?


  12. Peter Harrison

    That seems awfully quick. The data sheet for the PCD8544 (one of the typical controllers used) gives a maximum of 4MHz for the serial bus clock.
    You might want to check that your processor really is running as fast as you think it is.

  13. vikky

    thanx alot bro!! your code really helped us alot…

  14. alek

    When will you finish the proiect: where is schematics?

  15. Peter Harrison

    That project is quite old now. None of the schematics were public. Sorry.

  16. Odysseas

    Hello! I am new here trying to code the max6966 through esp32. I want to set max6966 to its default state which means 20ma on each output and keep in mind that my code is for esp32/arduino which is connected to the max6966 via j3-2/IO23 goes to DIN , j3-3/IO22 goes to CS and j3-19/CLK goes to CLK on max6966. Is the code below right for what I am trying to do?


    const int csPin = 22; // Chip Select pin for MAX6966 connected to IO22
    const int numPorts = 10; // Number of ports on the MAX6966

    void sendDataToMAX6966(uint16_t data) {
    digitalWrite(csPin, LOW);
    SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
    digitalWrite(csPin, HIGH);

    void initializeMAX6966() {
    sendDataToMAX6966(0x1005); // Set MAX6966 to run mode
    sendDataToMAX6966(0x0AFF); // Set all ports to high impedance (overwritten by intensity)

    for (int i = 0; i < numPorts; i++) {
    sendDataToMAX6966((uint16_t)i << 8 | 0xFF); // Set each port to active LED with max intensity (20mA)

    void setup() {
    SPI.begin(23, -1, 18); // Initialize SPI with IO23 (DIN) and IO18 (CLK)
    pinMode(csPin, OUTPUT);
    digitalWrite(csPin, HIGH);

    initializeMAX6966(); // Initialize MAX6966

    void loop() {
    // rest of code

  17. Peter Harrison

    I am afraid I have no experience with this chip and the ESP32.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.