Author Topic: M0 GPIO Output Power Setting  (Read 3256 times)

mantonakakis

  • NewMember
  • *
  • Posts: 20
M0 GPIO Output Power Setting
« on: April 16, 2019, 02:23:59 PM »
Hi everyone,

I'm working on a project that utilizes analog sensors that are essentially wired as voltage dividers, and I need low sleep power, thus I can't (simply) power the sensors from the 3v3 output of the regulator. The sensors each draw about 2.4mA, so assuming the specs given for the SAMD21 (by basically everyone that sells it) are correct, that each GPIO pin can supply 7mA, it should be no problem to power each sensor this way, especially if using pins on different clusters. All that should be needed is a digitalWrite high for the supply pin, analogRead the sensor output, digitalWrite low for the supply pin, then sleep.

However, when actually applying that load to the output pin and measuring the voltage, instead of 3.3V (my DMM usually reads 3.301V for no-load pin voltage), I was seeing about 2.97V. Not terrible, but a 10% error is definitely noticeable in the sensor readings. After a little searching, turns out the M0 has both a low-power and high-power mode for output pins, and low-power mode is 2mA, not 7mA - so my 2.4mA load might be a significant problem! To get 7mA, it seems two things need to happen, according to the datasheet in section 36.8.1:
  • VDD must be 3V-3.63V; no problem when Moteino M0 is powered by a lithium battery
  • PORT.PINCFG.DRVSTR = 1; definitely a problem in the Arduino IDE by default

I made a few changes to the Moteino samd core files to make this possible in the Arduino IDE, without having to jump through too many hoops. This is basically done in two ways, with some redundancy I think:
  • It seems like the most robust way is to set DRVSTR in the digitalWrite function (in wiring_digital.c and wiring_digital.h) to ensure it's configured for each digitalWrite.
  • The other way is to set DRVSTR in the pinMode function, but from what I've read, digitalWrite may override this setting, and indeed, if I use the standard digitalWrite function, setting DRVSTR in the pinMode function does not seem to last.

For the digitalWrite approach, I simply made an additional digitalWrite function (I called it digitalWriteHP) that is a copy of the normal function with a single line added directly after the pinMask definition in wiring_digital.c:
Code: [Select]
uint32_t pinMask = (1ul << pin); //existing code
PORT->Group[g_APinDescription[ulPin].ulPort].PINCFG[g_APinDescription[ulPin].ulPin].bit.DVRSTR = 1; //additional line
This also required the new function to be added to the wiring_digital.h file.

For the pinMode approach, I added a new mode in the pinMode function, called "OUTPUT_HIGH," and added a corresponding case in the switch. I copied the existing OUTPUT case and added the same line of code. This required a definition for OUTPUT_HIGH to be added to the wiring_constants.h file; I simply added:
Code: [Select]
#define OUTPUT_HIGH (0x4)
This alone, however, will not work, as the standard digitalWrite function seems to reset this value.

So now in the Arduino IDE I can do things like
Code: [Select]
pinMode(2, OUTPUT_HIGH);
or
Code: [Select]
digitalWriteHP(2, HIGH);
I think an even more seamless way to do this would be to add an argument to the digitalWrite function to represent the desired power state, so the normal calls can be made, and include a default argument to handle existing code, where the default is low-power. Perhaps another way would be to add an additional mode for the standard digitalWrite function, something like "HIGH_HP," and adding a corresponding case to the switch after the "LOW" case that copies and adds to the default case. I'll give this a try and see if it works.

Anyway, the results of setting the output pin to high power mode were, according to my CurrentRanger's OLED screen and my DMM:
  • in low-power mode: 2400-2402mA with 2.975V supplied
  • in high-power mode: 2423-2426mA with 3.211V supplied

It would be awesome if the Moteino M0 core files could be updated to add this functionality, since I'm sure other people will want to have more than 2mA available from an output pin, but if that's too much to ask, I hope this post helps at least for users to modify the files themselves. I'm going to try a few approaches and try to propose a best/simplest one, and post an update.

mantonakakis

  • NewMember
  • *
  • Posts: 20
Re: M0 GPIO Output Power Setting
« Reply #1 on: April 16, 2019, 03:08:55 PM »
Okay, I think I've settled on an approach that might work best. There's really no point in messing with the pinMode function, since it gets overridden by digitalWrite, so my ideal approach only changes the digitalWrite function. The standard function in wiring_digital.c looks like this:
Code: [Select]
void digitalWrite( uint32_t ulPin, uint32_t ulVal )
{
  if ( g_APinDescription[ulPin].ulPinType == PIO_NOT_A_PIN )
  {
    return ;
  }

  EPortType port = g_APinDescription[ulPin].ulPort;
  uint32_t pin = g_APinDescription[ulPin].ulPin;
  uint32_t pinMask = (1ul << pin);

  if ( (PORT->Group[port].DIRSET.reg & pinMask) == 0 ) {
    PORT->Group[port].PINCFG[pin].bit.PULLEN = ((ulVal == LOW) ? 0 : 1) ;
  }

  switch ( ulVal )
  {
    case LOW:
      PORT->Group[port].OUTCLR.reg = pinMask;
    break ;

    default:
      PORT->Group[port].OUTSET.reg = pinMask;
    break ;
  }

  return ;
}

My proposed change would be to add a new case called something like HIGH_HP. I got it to work with this simple change to the switch:
Code: [Select]
void digitalWrite( uint32_t ulPin, uint32_t ulVal )
{
  if ( g_APinDescription[ulPin].ulPinType == PIO_NOT_A_PIN )
  {
    return ;
  }

  EPortType port = g_APinDescription[ulPin].ulPort;
  uint32_t pin = g_APinDescription[ulPin].ulPin;
  uint32_t pinMask = (1ul << pin);

  if ( (PORT->Group[port].DIRSET.reg & pinMask) == 0 ) {
    PORT->Group[port].PINCFG[pin].bit.PULLEN = ((ulVal == LOW) ? 0 : 1) ;
  }

  switch ( ulVal )
  {
    case LOW:
      PORT->Group[port].OUTCLR.reg = pinMask;
    break ;

    case HIGH_HP:
      PORT->Group[port].OUTSET.reg = pinMask;
      PORT->Group[g_APinDescription[ulPin].ulPort].PINCFG[g_APinDescription[ulPin].ulPin].bit.DRVSTR = 1;
    break ;

    default:
      PORT->Group[port].OUTSET.reg = pinMask;
    break ;
  }

  return ;
}

In addition, HIGH_HP needs to be defined, so I added it to wiring_constants.h like so:
Code: [Select]
#ifndef _WIRING_CONSTANTS_
#define _WIRING_CONSTANTS_

#ifdef __cplusplus
extern "C"{
#endif

#define LOW             (0x0)
#define HIGH            (0x1)
#define HIGH_HP         (0x2)

#define INPUT           (0x0)
#define OUTPUT          (0x1)
#define INPUT_PULLUP    (0x2)
#define INPUT_PULLDOWN  (0x3)

#define PI 3.1415926535897932384626433832795
#define HALF_PI 1.5707963267948966192313216916398
#define TWO_PI 6.283185307179586476925286766559
#define DEG_TO_RAD 0.017453292519943295769236907684886
#define RAD_TO_DEG 57.295779513082320876798154814105
#define EULER 2.718281828459045235360287471352

#define SERIAL  0x0
#define DISPLAY 0x1

enum BitOrder {
LSBFIRST = 0,
MSBFIRST = 1
};

#ifdef __cplusplus
}
#endif
#endif

I think those two changes are all that's needed? It *should* keep all functionality of any existing code, and then to access the high-power output in Arduino IDE, all you need to do is digitalWrite(pin, HIGH_HP) instead of digitalWrite(pin, HIGH).
« Last Edit: April 16, 2019, 03:49:38 PM by mantonakakis »

Felix

  • Administrator
  • Hero Member
  • *****
  • Posts: 6866
  • Country: us
    • LowPowerLab
Re: M0 GPIO Output Power Setting
« Reply #2 on: April 16, 2019, 04:35:27 PM »
I appreciate the walkthrough and effort. The changes seem straight forward but I wonder if there are any other reasons the high power mode was not enabled by default.
Would it be possible to just call PORT.PINCFG.DRVSTR = 1 before each call to power your sensor?

mantonakakis

  • NewMember
  • *
  • Posts: 20
Re: M0 GPIO Output Power Setting
« Reply #3 on: April 16, 2019, 05:19:06 PM »
I appreciate the walkthrough and effort. The changes seem straight forward but I wonder if there are any other reasons the high power mode was not enabled by default.
Would it be possible to just call PORT.PINCFG.DRVSTR = 1 before each call to power your sensor?
Yeah, I don't see any reason that wouldn't work - for some cases that would be very easy, but the more times you need to power the sensor in your sketch, obviously the more clunky it becomes to need an extra line of code each time (in my case, with three sensors, I'm finding my last solution to be very easy - and if I need high-power output in any future sketches, it's just 3 extra characters added to my normal digitalWrite call). I guess it's also worth pointing out that the same feature exists when setting the pin low, with a little extra current (10mA vs. 7mA).

It's unfortunate that Arduino IDE hasn't addressed this when the official Arduino M0 boards claim 7mA per pin on their product pages, yet it isn't the default action. I imagine many people buying M0 boards won't realize that this is not the default, and likely won't be able to easily figure out how to enable it (it doesn't seem to be mentioned in any official Arduino documentation for the boards). I stumbled upon this feature of the SAMD21 mostly by accident, and it took a bit of luck to find the easiest solution. I happened to find it because I plugged in my sensors one at a time, and noticed that with one sensor, the reading seemed a little low, but reasonable; with two, the reading dropped about 40%; and with three, all sensors read full low, meaning they weren't getting any power!

I am thoroughly enjoying working with the Moteino M0 though, so thank you so much for developing it! It's so far greatly outperforming several other alternatives for my project (battery-powered LoRa node with enough memory for the LMIC library). With my current project, including RFM95, FRAM, and three analog sensors, my CurrentRanger is showing ~7uA pulled from a LiPo battery while in sleep mode :)
« Last Edit: April 16, 2019, 05:21:44 PM by mantonakakis »

Felix

  • Administrator
  • Hero Member
  • *****
  • Posts: 6866
  • Country: us
    • LowPowerLab
Re: M0 GPIO Output Power Setting
« Reply #4 on: April 16, 2019, 05:54:34 PM »
I think adding the ability to use the high power mode of the pins is a nice-to-have. The thing that is sort of confusing is why they are even optional.
Adding an unknown override or function that does it is almost as obscure as not knowing there is that option and its OFF by default.
I will consider making it a default (ie just do it in the digitalWrite), but my only concern is causing hardware problems having it ON. I just don't have enough information to make a fully educated decision. I want to avoid at all cost the possibility of causing harm to those M0's out there, where the user is not even aware this is turned ON and they are not looking to use the high power output of the pins.

I'm very happy to hear you like the M0's features and low power. I worked hard to achieve the M0 and I do consistently receive great feedback.
The best compliment I get is when you pass the word of mouth around to your friends and colleagues.

mantonakakis

  • NewMember
  • *
  • Posts: 20
Re: M0 GPIO Output Power Setting
« Reply #5 on: April 22, 2019, 11:47:28 AM »
I will consider making it a default (ie just do it in the digitalWrite), but my only concern is causing hardware problems having it ON. I just don't have enough information to make a fully educated decision. I want to avoid at all cost the possibility of causing harm to those M0's out there, where the user is not even aware this is turned ON and they are not looking to use the high power output of the pins.
Makes perfect sense, I haven't read every word of the datasheet, but I don't recall them going into great detail about why this two-mode functionality is present in the first place.
I'm very happy to hear you like the M0's features and low power. I worked hard to achieve the M0 and I do consistently receive great feedback.
The best compliment I get is when you pass the word of mouth around to your friends and colleagues.
It's been very refreshing to find someone with your approach/mindset for developing these boards - they have been exactly what I've been looking for, and your support and availability is awesome. The niche you have carved is greatly appreciated and you will definitely have my recommendation whenever I'm in a position to give it! And at the very least we will be ordering more Moteinos soon :)
« Last Edit: April 22, 2019, 11:49:07 AM by mantonakakis »

Felix

  • Administrator
  • Hero Member
  • *****
  • Posts: 6866
  • Country: us
    • LowPowerLab
Re: M0 GPIO Output Power Setting
« Reply #6 on: April 22, 2019, 01:23:35 PM »
Thanks for the feedback, much appreciated.

BTW take a look at how this core deals with this problem, it enables the high drive on all the pins:
https://github.com/mattairtech/ArduinoCore-samd/tree/master/variants/MT_D21E

Look for this directive in the variant.cpp file: PER_ATTR_DRIVE_STRONG

Mr6510

  • NewMember
  • *
  • Posts: 1
Re: M0 GPIO Output Power Setting
« Reply #7 on: April 25, 2020, 08:53:47 AM »
@mantonakakis thanks for posting about this, I think I just followed in your footsteps working through the SAMD21 data sheet and learning about DRVSTR etc.  But was not able to code a solution until I found these posts. I have taken some of your code now and created my own high power digitalWrite and I'm now able to deliver the 5mA per port (with port clustering limitations taken into consideration) for my project.
Does seem crazy that it's well into 2020 and nothing has been done in the central code to eliminate the issue.
Thanks for documenting out your thinking & code ;)
Cheers.