Arduino I2C Slave Peripheral

With a recognition of the existing gap, I present this contribution aimed at enhancing the publicly accessible guidelines for laying out and coding Arduino I2C slave devices. This proposal introduces an organizational framework for coding I2C slave peripherals on Arduino, illustrated through a Platformio-based example.

Structure

Using an Atmel328PB microcontroller, we simulate a series of 16-bit peripheral registers, each with an 8-bit address. These registers are programmed akin to standard silicon I2C devices, allowing individual configuration for read-only, write-only, or read-write operations as perceived by the I2C master.

/*****************************************************************************
 *
 *                              Global Variables
 *
 *****************************************************************************/
// An array to store register values
int i2c_registers[I2C_NUM_REGS] = {0};

// I2C session persistent register address
uint8_t registerAddr = 0xFF;

In the development of an I2C slave application, peripheral functionality must be implemented through data source and data sink operations, skillfully integrated with the aforementioned emulated device registers.

In scenarios demanding highly efficient application code execution, integration occurs within the I2C interrupt service context. These application operations are executed dynamically during active I2C communication. However, integrating time-consuming application operations here can potentially delay I2C timing to a degree that compromises standards compliance, resulting in communication failures.

To mitigate this risk, in cases where application code runs slower, linkage between data source/sink operations and emulated peripheral registers occurs outside the I2C service interrupt context, typically within or under the task loop scope. Despite this approach, simultaneous access to emulated registers can occur, due to I2C ISR service routine triggering, leading to data integrity issues.

To ensure robustness, a pair of interrupt-suspending memory access macros is provided. These macros guarantee atomicity during peripheral register data manipulation within the task loop, thereby preventing potential corruption

I2C Call-back Functions

During program setup, the I2C bus under configuration, is assigned a device address, and call-back routines for the service of wired I2C bus transactions. When an I2C-inbound write occurs, data is consumed and/ or is stored in a register, with code inside the receiveEvent() callback. When an I2C-inbound read occurs, data is application generated, or is retrieved from a register, inside the requestEvent() ISR.

  Wire.begin(I2C_ADDRESS);      // Initialize I2C communication as a slave
  Wire.onReceive(receiveEvent); // Register the receive event handler
  Wire.onRequest(requestEvent); // Register the request event handler

The unpopulated receiveEvent() call-back

An unpopulated receiveEvent() ISR services wired I2C register storage instructions. Not yet linked to any application functionality, it merely provides write access to peripheral register memory.

/*****************************************************************************
 *
 *                               receiveEvent()
 *
 *****************************************************************************/
// Event handler for receiving data from the master, to write to a register
void receiveEvent(int numBytes) 
{ 
  // Read the requested register address
  registerAddr = Wire.read(); 
  
  if (numBytes == 3) 
  {
    // Read the data to write
    uint16_t value = Wire.read() ;        // value LSB
    value |= Wire.read() << 8;            // value MSB

    switch (registerAddr)
    {
      // Comment-out any read-only registers to prevent master write
      case I2C_REG_0:
      case I2C_REG_1: 
      case I2C_REG_2:
      case I2C_REG_3:
      case I2C_REG_4:
      case I2C_REG_5:
      case I2C_REG_6:
      case I2C_REG_7:
      {
        i2c_registers[registerAddr] = value;
        break;
      }
      default: return;
    }

    I2C_WRITE_DEBUG(registerAddr, value)
  }
}

The unpopulated requestEvent() call-back

An unpopulated requestEvent() ISR services wired I2C register retrieval instructions. Not yet linked to any application functionality, it merely provides read access to peripheral register memory.

/*****************************************************************************
 *
 *                               requestEvent()
 *
 *****************************************************************************/
// Event handler for responding to requests for register contents, from the master
void requestEvent() 
{
  uint16_t value = 0;

  switch (registerAddr)
  {
    // Comment out any write-only registers to prevent master read
    case I2C_REG_0:
    case I2C_REG_1:
    case I2C_REG_2:
    case I2C_REG_3:
    case I2C_REG_4:
    case I2C_REG_5:
    case I2C_REG_6:
    case I2C_REG_7:
    {
      value = i2c_registers[registerAddr];
      break;
    }
    default:
      return;
  }

  // Send the data read from
  Wire.write(value & 0xFF); // send LSB
  Wire.write(value >> 8);   // send MSB

  I2C_READ_DEBUG(registerAddr, value);

  return;
}

Read-ability and write-ability

So, we may see from the above, in the unpopulated ISR pair, an I2C master has the ability to write a value to one of a number of memory-only registers, and then read it back from there verbatim. We’ll see later, when we come to adding applications, such simultaneous read-ability and write-ability may not be desirable.

To make a peripheral register read-only, we remove the receiveEvent() ISRs ability to service peripheral register write requests. We do this by commenting-out service routine switch cases that involve the register.

void receiveEvent(int numBytes) 
{

...

    switch (registerAddr)
    {
      // Comment out any read-only registers to prevent master write
      case I2C_REG_0:
      //case I2C_REG_1:  // Make register 1 read-only
      case I2C_REG_2:
      case I2C_REG_3:
      case I2C_REG_4:
      case I2C_REG_5:
      case I2C_REG_6:
      case I2C_REG_7:
      {
        i2c_registers[registerAddr] = value;
        break;
      }
      default: return;
    }

...

To make a peripheral register write-only, we remove the requestEvent() ISRs ability to service peripheral register read requests. Again, we do this by commenting-out service routine switch cases that involve the register.

void requestEvent() 
{

...

  switch (registerAddr)
  {
    // Comment out any write-only registers to prevent master read
    case I2C_REG_0:
    case I2C_REG_1:
    // case I2C_REG_2:  // Make register 2 write-only
    case I2C_REG_3:
    case I2C_REG_4:
    case I2C_REG_5:
    case I2C_REG_6:
    case I2C_REG_7:
    {
      value = i2c_registers[registerAddr];
      break;
    }
    default:
      return;
  }

...

Application linkage inside loop()

Now that we have our peripheral registers configured, variously as read and write, read-only, or write-only, we come to linking these to application code. This involves the data sink and data source routines, that you will develop for your peripheral.

Remember, all access to peripheral registers outside the I2C interrupt service context, must employ the atomic register access macros, I2C_ATOMIC_REG_RD() and I2C_ATOMIC_REG_WR(). Again, this is vital to prevent the data corruption that occurs, when I2C ISRs are invoked, during peripheral register access. The macros momentarily ‘lock-out’ the ISRs.

/************************************************************************
 *
 *                                loop()
 *
 ************************************************************************/
void loop() 
{
  {
    uint16_t value = 0;

    // Read adc, and atomically save val in read-only slave register 1
    value = myadc_read();
    // i2c_registers[I2C_REG_1] = value; <- not atomic, instead...
    I2C_ATOMIC_REG_WR(i2c_registers[I2C_REG_1], value);

    // Retrieve contents of slave register 0 atomically, and consume
    // value = i2c_registers[I2C_REG_0]; <- not atomic, instead...
    I2C_ATOMIC_REG_RD(value, i2c_registers[I2C_REG_0]);
    myservo_set_pos(value);
  }

  // Other application tasks.

}

In the above, we see that a logically read-only peripheral register, I2C_REG_1, is fed samples from an onboard ADC, using an application call to myadc_read(). A remotely wired, I2C master may read the stored register data, with calls such as wiringPi’s wiringPiI2CReadReg16().

As well, above, we have an either read and write, or write-only configured peripheral register, I2C_REG_0, feeding position data to an attached servo. It does this via an application call to myservo_set_pos(). A remotely wired, I2C master may write the servo position data, with calls such as wiringPi’s wiringPiI2CWriteReg16().

On-demand, vs polled register access

The above mentioned approach for linking application data sink and source functionality, to underlying peripheral register memory, from outside I2C ISR service context, can be wasteful. Significant cpu cycles are consumed, in the application either constantly polling peripheral registers for incoming data, or frequently updating peripheral registers for fresh outgoing data.

To increase application efficiency, an alternate user code linkage solution exists, for on-demand exchange of data with peripheral registers. This involves application data source and data sink function calls, from within I2C ISR service context.

As mentioned previously, application calls made inside either of the I2C ISR call-backs, must be very brief, so as not to disrupt wired I2C communication. Between the servo and ADC application calls dealt with previously, only the myservo_set_pos() call is suitable. The myadc_read() function involves lengthy over-sampling, and any attempt to link it ‘on-demand’, from within an ISR, will break associated I2C transactions.

Application linkage inside the I2C ISRs

Here we examine the alternative approach to linking an I2C slave peripheral’s servo position. Data is exchanged only when requested to be, by a wired I2C bus transaction. The I2C receiveEvent() ISR handles incoming data, and we merely write that ‘value’ data to the servo, using the necessarily fast myservo_set_pos() application call.

void receiveEvent(int numBytes) 
{

...

    switch (registerAddr)
    {
      // Comment out any read-only registers to prevent master write
      case I2C_REG_0:
      {
        myservo_set_pos(value);
        i2c_registers[registerAddr] = value;
        break;
      }
      //case I2C_REG_1:  // Read-only
      case I2C_REG_2:
      case I2C_REG_3:
      case I2C_REG_4:
      case I2C_REG_5:
      case I2C_REG_6:
      case I2C_REG_7:
      {
        i2c_registers[registerAddr] = value;
        break;
      }
      default: return;
    }

    I2C_WRITE_DEBUG(registerAddr, value)
  }
}

...

In this case, we have chosen to make the servo peripheral register both readable, and writeable. To accomplish this, we must also store the incoming data in the underlying peripheral register, as shown above.

In the case where we wanted to make the application’s servo peripheral register write only, we would not store the incoming position data in any underlying peripheral register, or, at the very least, make the underlying register write only. This is again accomplished, by commenting-out the appropriate switch statement case, inside the requestEvent() ISR.

void requestEvent() 
{

...

  switch (registerAddr)
  {
    // Comment out any write-only registers to prevent master read
    //case I2C_REG_0:  // Make servo register 0 write-only
    case I2C_REG_1:
    case I2C_REG_2:
    case I2C_REG_3:
    case I2C_REG_4:
    case I2C_REG_5:
    case I2C_REG_6:
    case I2C_REG_7:
    {
      value = i2c_registers[registerAddr];
      break;
    }
    default:
      return;
  }

...

For the final case, we examine how outgoing application data may be linked to peripheral register requests, on-demand, from within I2C ISR context. For this, we will read an attached switch, with an imaginary application call, myswitch_get_posn(). We know that the call will be fast enough not to break wired I2C communication, and that logically the operation must be read-only.

The I2C requestEvent() ISR handles outgoing data, and we merely service a request for the switch position, with data from the myswitch_get_posn() application call.

void requestEvent() 
{

...

  switch (registerAddr)
  {
    // Comment out any write-only registers to prevent master read
    case I2C_REG_2:
    {
      value = myswitch_get_posn();
      break;
    }
    case I2C_REG_0:
    case I2C_REG_1:
    case I2C_REG_3:
    case I2C_REG_4:
    case I2C_REG_5:
    case I2C_REG_6:
    case I2C_REG_7:
    {
      value = i2c_registers[registerAddr];
      break;
    }
    default:
      return;
  }

...

As seen above, the switch peripheral register is logically read only, so stored peripheral register memory is not involved. To disable any attempt to write a peripheral register, making it read only, we comment-out it’s associated entry in the receiveEvent() I2C ISR.

void requestEvent() 
{

...

  switch (registerAddr)
  {
    // Comment out any write-only registers to prevent master read
    case I2C_REG_0:
    case I2C_REG_1:
    // case I2C_REG_2:  // Make switch register 2 write-only
    case I2C_REG_3:
    case I2C_REG_4:
    case I2C_REG_5:
    case I2C_REG_6:
    case I2C_REG_7:
    {
      value = i2c_registers[registerAddr];
      break;
    }
    default:
      return;
  }

...

Improving polled access efficiency

/************************************************************************
 *
 *                                loop()
 *
 ************************************************************************/
void loop() 
{
  {
    uint16_t value = 0;

    if (myservo_changed())
    {
      // Retrieve contents of slave register 0 atomically, and consume
      I2C_ATOMIC_REG_RD(value, i2c_registers[I2C_REG_0]);
      myservo_set_changed(0);

      // From here on, receiveEvent() ISR can take new register data,
      // and the snapshot 'value' can be processed, even if exhaustively
      myservo_set_pos(value);
    }
  }

  // Other application tasks.

}
void receiveEvent(int numBytes) 
{

...

    switch (registerAddr)
    {
      // Comment out any read-only registers to prevent master write
      case I2C_REG_0:
      {
        i2c_registers[registerAddr] = value;
        myservo_set_changed(1);
        break;
      }
      case I2C_REG_1:
      case I2C_REG_2:
      case I2C_REG_3:
      case I2C_REG_4:
      case I2C_REG_5:
      case I2C_REG_6:
      case I2C_REG_7:
      {
        i2c_registers[registerAddr] = value;
        break;
      }
      default: return;
    }

    I2C_WRITE_DEBUG(registerAddr, value)
  }
}

...

Well, that’s how the I2C slave peripheral model works, how to constrain access to it’s underlying peripheral registers, and how to link-in application code. Application linkage methods were shown, both by polling inside loop(), and by on-demand calls inside ISR context.

Just remember to keep application calls short inside the ISRs, and to use atomic register access macros outside them.

Associated Files:

The attached Arduino I2C slave demonstration code for this example, runs on an AT328PB. It reads the position of an ADC-connected potentiometer, and stores this data in a 16-bit I2C register numbered 1. It also reads data in a 16-bit I2C register numbered 0, and sets the corresponding position of an attached servo.

Example C code for a wiringPi-installed, Raspberry Pi master is also provided, which remotely reads the potentiometer value, and writes a proportionate value back to the I2C slave peripheral’s servo control register.

Demonstration code, of the suggested organisational paradigm, for an Arduino I2C slave peripheral. I2C_slave_model.zip

WiFi FPV Robots

I detail the make of WiFi Robots, with first person video, sensory feedback, and actuator control, from ‘toy hacked’ remote control toys.

WiFi FPV Robot 1

WiFi FPV Robot 1, featured above, is built upon a 2.4GHz RC Rock crawler chassis. The fitted Raspberry Pi Zero W streams video to an HTTP browser session, from which the operator can gain status feedback, and control motors and lights with a game controller.

The project was unique, in that it successfully employed dc brushed motor Back-EMF measurements, for fine motor control. The inbuilt Arduino 328P Nano, is tasked with motor feedback sampling and PID control, motor PWM outputs, as well as lighting control, status assessment and reporting.

WiFi FPV Robot 2 (WIP)

The plan is to acquire a mecanum wheeled version of the un-branded RC Rock Crawler used in V1, and convert it using many of the same techniques and technologies employed previously.

The updated mecanum rc toy runs a AUD$55 investment, though?


11/04/2024 – Component accumulation ensues…


22/04/24 – Parts still amassing.


ARDUINO COMPONENT

Interested in Raspberry Pi and Arduino robotics? Why not check-out my Arduino I2C Slave Peripheral Paradigm? There’s a brief write-up on how to use it, as well as sample code to download.


23/04/24 – The front connector for a Raspberry Pi Zero 2W.


24/04/24 – That’s I2C and power to the Pi Zero 2W, and an unpopulated IDC header to wire any remaining Pi pin, if ever required.


26/04/24 – The autopsy commences. I will measure motor currents first, before gutting, then build the electronics bay up large enough to accommodate PSUs, H-Bridges, and feedback signal conditioning. – Loaded motors current was ~2A or more. I’ll be splitting that between dual 1.5A supplies. The remaining half ampere on each, is exhausted by CPU and microcontroller power on one converter, and by more extensive lighting on the other.


27/04/24 – Four motors, means 4 signal conditioner circuits. They rectify, filter, scale and clip, motor back EMF signals for ADC conversion. This is the signal we use to control engine power, using PIDs. Perf board turned out to be a good choice, and the whole board was done in 2 hours, or so. Used less space than budgeted.


28/04/24 – 65-degree field of view, 5Mp OV5647 camera, gets a 120-degree FOV lens upgrade, courtesy of a cheap OV2640 donor camera. On the second attempt, I sanded the square flange right off the donor’s lens mounting ring. With SuperGlue, I tacked it to the old lens mount, where it’s lens ring had been sanded away. A final skin of epoxy, secures the 2 lens fixtures together, as well to the camera image sensor. The result was flawless.- I couldn’t find a wide angle lens equipped camera, for my Pi Zero 2W’s in-housing v2 camera, so I made one…hacking defined.


28/04/24 – Twin 1.5A Buck Boost power converters installed in the battery compartment. Loads of room for the H-Bridge and signal conditioner above.


ARDUINO COMPONENT

I’ve just finished the Arduino I2C slave peripheral code, which I’ll be using soon, and you can see the full write-up, and download the demo source over here.

As a foundation upon which to build register-oriented, I2C applications. These, typically in the control and data acquisition genre. The solution I provide in ‘Arduino I2C Slave Peripheral Paradigm’, is a good choice to build your application code over. The user implements both data source and data sink application functions, and link these to the underlying, emulated register set. The methods are fully documented, and the code has been ‘hardened’, during intense peer review.


Side and Rear Lights

30/04/24 – Marking, then carving up the enclosure; 8 square holes, mounting and wiring of 8 LEDs.

More WS2812 lights coming, for a total around 300mA. Adding in the original toy’s underbody lighting, will draw another 30+mA, as they’re being overdriven.


Body on Chassis

30/04/24 – Saw-milled dowel from a hardwood plank, to make the body/ chassis interstitial rails. Drill press used to bore the 4mm holes.

Everything fits, some polishing to do.


STM32F103C8T6 Blue Pill

I’ve been using an AT328PB microcontroller for development up to this point. It has decided to refuse to fully connect and program now, so I’m done with that device. It was never really suited to running 8 PWMs, and was looking like too much bother, anyhow.

So, now I’m looking at STM32F103C8T6 Blue Pill board, making sure which pins are available to run things like PWMs, DACs, etc.

Blue Pills were once a pain to work with, though ST’s USB Bootloader, has made it pleasure (but a setup hurdle).

15 PWM outputs!!! Buckets of CPU to run the 4 PID loops. Hope everything else works out.


Slave development and wiring

2/05/24 – Got code for an I2C slave with servo and potentiometer working. Different Servo library. Got Neopixel drive working, with a different STM32 library.

Will try to bring the full set of peripheral hardware up on the Blue Pill later tonight. So far, it’s looking good for application fit.


3/05/24 – Can now get 8 PWM outputs with Arduino calls, but only at 1kHz, and only on 8 of the 10 ADC inputs. We need 5 ADC inputs, so have to setup alternate PWM hardware myself.

Now, can get 5 PWM outputs, from 7 in non ADC group, to work. 2 taken by I2C. Need to figure out how to drive 4 reversible motors, using only 1 PWM for each. That’s extra logic to design and build.

Neopixel module looks great, but makes my I2C unstable. !

Extra logic

To drive the 8 H-Bridge motor inputs, with just 4 PWM outputs, we need some extra logic. Octal gated switches, to be precise.

Oh, how we love to hand wire daughter boards.


Slave Motherboard gets a Daughter


5/05/24 – Have the Octa-Switch tested, debugged and fully documented. ChatGPT seems to think that it should be called a quad duo-switch. Whicheither!


7/05/2024 – Got the Slave Controller’s Motherboard likely finalised, and then documented.


7/05/24 – FastLED refuses to work on the Blue Pill. Might be able to fix AdaFruit’s NeoPixel code, and stop it crashing I2C, but my STM32F103C8T6 Blue Pill Board has gone to heaven. Replacement board will have double flash size, and get here in ~12-days. Kind of a spanner, but I can re-order loads of other tasks.


8/05/24 – Blue Pill remains intermittent. WS8212 Neopixel for light bar arrived. Tested, and fortunately compatible with other LEDs. Wired-up, so that’s just the H-Bridge/ signal conditioner PCB to finish wiring.

All basic internal wiring complete, enough to run CPUs, motors and lights. Won’t power-up, so some tracing to do. Back to the dodgy bootloader problem first. Several bootloader options, dunno which is best.


9/05/24 – Given up on USB bootloaders, in favour of an ST-Link v2 USB debugger. The USB debugger was a real boon, but failed to connect reliably for programming.

First light from all 16 NeoPixels, photo does no justice.


11/05/24 – Sick as a dog.


12/05/24 – Extant Blue Pill board has developed a short from 3.3v to GND. Toast. New Blue Pill+ might arrive on Friday. I’ll be very interested to see how the USB Bootloader for a high density F103CB device works. Debugger is better, but hid device is just so easy.


13/05/24 – Basic rover hardware tested and working. Will be slow-going until replacement Blue Pill+ arrives. More care not to reverse bias the LDO on the new one.


HMC5883L

Adding this HMC5883L magnetometer early in the build, as it will be used to help control mecanum yaw motion. I’ve previously written a Compass module in C, that gives accurate compass degrees, either planar, or from a known tilt angle. We’ll use the planar compass routine here.


6Ah 8.4V Battery

~50Wh, 2S2P, 4 x 18650 3000mAh 15A NMC LiPo, slanted to reduce height.


14/05/24 – 3-motors and 1 PID loop in the first WiFi Robot was tricky enough. Now there are 4-motors and 4 PID control loops to code. All whilst getting one’s head around the permutations of this:


Getting Arduino AVR Disassembly with Platformio

You can get a cpp-source-interleaved disassembly listing, when compiling for Arduino AVR with Platformio.

...

void loop() 
{
  // Read adc and save result in read-only slave register 1
  i2c_registers[I2C_REG_1] = myadc_read();
     cc2:	0e 94 46 06 	call	0xc8c	; 0xc8c <_Z10myadc_readv>
     cc6:	90 93 21 01 	sts	0x0121, r25	; 0x800121 <i2c_registers+0x3>
     cca:	80 93 20 01 	sts	0x0120, r24	; 0x800120 <i2c_registers+0x2>

...

15/05/24 – Have ‘roughed-out’ control for 4 PID motor drivers, each with stall recovery, FAA lighting control, and battery voltage monitor.

Code structure needs much work, before dressing-up for presentation. Very productive coding session, lots of flow.

On-track for Blue Pill+ delivery on Friday, 2-days hence. If not, then Monday.

Time to ride=-🚴


16/05/24 – Auspost says the Blue Pill+ will be delivered today. Better get cracking and properly structure the motor driver, light control and voltage sensing solution.

Later:

Blue Pill+ arrived quite early. USB HID Bootloader is just as rubbish, so straight back to the debugger for programming. Plus variant pinout turns out to be different for 1 power pin. It shorted the 5V, when I plugged it into the hardware. Also, onboard indicator LED also changed.


17/05/24 – All 4 PWM motor drivers working, with motor back EMF feedback and PID control for each. FAA LEDs also working. Restructure complete. Tiny bit of optimisation, then dress-up for presentation, and we can move-on to mecanum moves.

Taking a slow day, as pooped from last few days frantic coding.


18/04/24 – Basic PID tune in place, stall detection debugged and roughly attuned.

Things are starting to come together, with mecanum motion motor patterns:


19/05/24 – Come to actually use that I2C slave code library you spent weeks agonising over every detail of, and you find it doesn’t work, still?!?!

As it looks, this could take a bit. Problem is with the Arduino Wire library, running on STM32 hardware.

Later: Cracked it. Default 100kHz Wire functionality problematic. Getting both master and slave up at 400kHz bothersome, but mostly successful. Time for a reward.🚴‍♂️


20/05/24 – All but 1 bug eliminated from I2C slave code. Needed something to command it, so skipped-ahead and re-wrote the I2C master code from first WiFi FPV Robot, so it now issues new moves and instructions. Will use it to debug the slave code tomorrow, when both the robot’s batteries, as well my own, are recharged.

Actually quite close to the end result, of basic FPV interaction. Just some JavaScript and PHP for the server to be re-written. Not bothering with On-screen visual feedback of controller inputs this time, as the mecanum movements don’t translate. That will make things far easier.


21/05/24 – Busy debugging the i2c part of the slave. Responds to most of the commands depicted above, yet won’t run any motors.

Later: Have the full i2c link working. On the master Raspberry Pi, you issue a command like i2c_master –LIGHTS_ON, and the Arduino slave now responds appropriately.

I2C comm link will be complete, after the servo driver is integrated into the slave, and a means to set it’s value is inserted into the i2c_master program.

Later again: Disaster! A timer conflict, between Arduino PWM outputs, and their Servo library.


22/05/24 – Bogged-down trying to find a solution to drive a servo off timer 2. Can’t get any of the public servo libraries to work. Not above writing my own servo driver, but then find public timer libraries are also shit. Okay, not above writing my own timer code, too!


24/05/24 – Still bogged, trying to get STM32CubeMX projects to compile on Platformio. Working toward a Timer library for Arduino. Drama downloading ST software, won’t be resolved today.


25/05/24 – Have the timer working as an STM32CubeMX project under Platformio (Gasp!). Now, to figure-out how to run just the timer code, as an Arduino library.


26/05/24 – Finally! As an Arduino library, using the Arduino STM32 HardwareTimer library, may I present, An up to 8 Positive-Pulse PWM Servo Driver.

Okay, 8 Positive-Pulse PWM Servo Driver library for Arduino, now integrated with I2C control of remaining mecanum robot functionality. Works great. Login to the Raspberry Pi 2W and type ‘i2c_master –SET_SERVO 127’, and hear the tiny 9g servo zip to it’s centre. No discernible latency, whatsoever!

Later: Fixed a sequencing error in the lighting system, and added a low-pass filter to the stream of battery voltage measurements. The battery voltage readings were very noisy when all four mecanum motors ran, and adding a couple of 1uF ceramics across the ADC input didn’t help much.


28/05/24 – Have build drivers for, and debugged both the HMC5833L magnetometer, and the VL53L1 time of flight distance sensor. Both of these have been integrated into the i2c_master program, that runs on the Pi Zero 2W. The three nodes on the Pi Zero 2W’s mastered i2c network are, the STM32 acting as real-time dog’s-body, the TOF distance sensor, and the magnetometer compass.

Now, we need to expand and modify, the PHP file that calls the i2c_master program, with parameters based on the webserver PHP file’s HTTP GET request.

Later:


30/05/24 – Whilst prototyping new ideas for the WiFi FPV Robot’s main webpage, we’ve discovered an issue.

i2c runs fine for a time, but then there’s the inevitable crash. The Pi Zero 2W hosted i2c_master runs afresh every repeated invocation, so that’s not primarily suspicious.

The error appears to occur for voltage retrieval, with associated voltage reports of -0.1, and 0.0. Everything stops as far as i2c comms goes, and there’s a strange pattern on the i2c lines:

So, which is it? The i2c slave code gone screwy, or the Raspberry Pi Zero 2W’s i2c comms subsystem that needs a reset? Nothing in dmesg. Thinking caps on.

Later: Optimised one bit of code in i2c_master, that prints the battery voltage as program output. After that, and running the webserver only, and not invoking the windowing subsystem, all the i2c problems went away.

All systems are go for completion of the final web interface. Prolly should include some mecanum moves, that in hindsight would benefit completion of the joystick interface, and with which previously I hadn’t dealt.


1/06/24 – Finalised and tested the Mecanum movement set, and all associated control code. Working on expanding the web browser UI section that reports status, to nine units of real-time data, plus an additional reboot button. But not tonight.


1/06/24 – Got the entire instrument panel working, as well as lights control, gear changing, and control of the servo position. Tiny bit of tidying up here to do.


4/06/24 – In addition to a complete instrument set, all code for movement based on joystick inputs is complete.

i2c is back to plague me. Push it too fast, and it falls over. Plan is to optimise some sections of code, and see if that doesn’t get me the CPU needed for i2c stability. I’ve done zero optimisation on this project, wanting to see what the STM32F103CB could do. Honestly, I’m surprised it’s coping with almost exclusively floating point math.


5/06/24 – Not knowing where to start with the i2c fault, I’ve gone off isolating potential problem areas, and carefully observing the results. It turns out I have a flawless i2c link when battery voltage is 8.1V, and an almost instantaneously broken one at 7.1V. In probing around at some voltages, I found that the ground level on my PI’s mounting board, is in difference to the one on my Blue Pill +. I expect I’ll have something driveable, as soon as I fix that.

But that will likely be body-off-chassis surgery to rectify that, so tomorrow will have to do.

Later: …and that turned out to be a wild goose chase. Still no i2c resolution.


6/06/24 – Starting to think that the STM32 Arduino Wire library may be the cause. I’ve isolated every subsystem, and twiddled every conceivable contention source, yet i2c remains unstable. If I can’t manage a graceful fault recovery, then we may be looking at a native STM32CubeMX re-wite.

Very limiting, is the discovery that the STM32 Arduino Wire library is deficient. Ordinary Arduino Wire calls, that might have gotten me out of i2c peril, just aren’t implemented in the STM32 version. Can I paste-in some low-level operations, inside the Arduino code?


10/06/24 – Narrowed the i2c problem down to Adafruit’s NeoPixel library, with it’s STM32 code turning off interrupts for the entirety of the time it emits pixel commands (breaking i2c), and screwing with the SysTick timer. Curious that it works at all.

May have to code a suitable replacement. Could cheat, and throw an Arduino Pro Mini at the problem, then go nuts coding extra lighting effects? No, we’ll persevere with the STM32F103CB for now.


16/06/24 – Working on an Arduino compatible WS2812 NeoPixel driver, that doesn’t screw with either the SysTick timer, interrupts or exceptions. DMA fed PWM timers are out of the picture, as Arduino’s use of HAL calls makes this impractical. Yet, I have one trick left up my sleeve, and it involves (ab)using the STM32’s DWT counter. I’ll wanna take my time and get it right, as so many other Arduino users find themselves with similar NeoPixel driver issues.