Przeglądaj źródła

Addition of gpio.pulse functions (#2190)

Philip Gladstone 6 lat temu
rodzic
commit
77fe51050a
4 zmienionych plików z 611 dodań i 4 usunięć
  1. 1 0
      app/include/user_modules.h
  2. 11 0
      app/modules/gpio.c
  3. 431 0
      app/modules/gpio_pulse.c
  4. 168 4
      docs/en/modules/gpio.md

+ 1 - 0
app/include/user_modules.h

@@ -37,6 +37,7 @@
 #define LUA_USE_MODULES_FILE
 //#define LUA_USE_MODULES_GDBSTUB
 #define LUA_USE_MODULES_GPIO
+//#define LUA_USE_MODULES_GPIO_PULSE
 //#define LUA_USE_MODULES_HDC1080
 //#define LUA_USE_MODULES_HMC5883L
 //#define LUA_USE_MODULES_HTTP

+ 11 - 0
app/modules/gpio.c

@@ -315,12 +315,20 @@ static int lgpio_serout( lua_State* L )
 }
 #undef DELAY_TABLE_MAX_LEN
 
+#ifdef LUA_USE_MODULES_GPIO_PULSE
+extern const LUA_REG_TYPE gpio_pulse_map[];
+extern int gpio_pulse_init(lua_State *);
+#endif
+
 // Module function map
 static const LUA_REG_TYPE gpio_map[] = {
   { LSTRKEY( "mode" ),   LFUNCVAL( lgpio_mode ) },
   { LSTRKEY( "read" ),   LFUNCVAL( lgpio_read ) },
   { LSTRKEY( "write" ),  LFUNCVAL( lgpio_write ) },
   { LSTRKEY( "serout" ), LFUNCVAL( lgpio_serout ) },
+#ifdef LUA_USE_MODULES_GPIO_PULSE
+  { LSTRKEY( "pulse" ),  LROVAL( gpio_pulse_map ) }, //declared in gpio_pulse.c
+#endif
 #ifdef GPIO_INTERRUPT_ENABLE
   { LSTRKEY( "trig" ),   LFUNCVAL( lgpio_trig ) },
   { LSTRKEY( "INT" ),    LNUMVAL( INTERRUPT ) },
@@ -336,6 +344,9 @@ static const LUA_REG_TYPE gpio_map[] = {
 };
 
 int luaopen_gpio( lua_State *L ) {
+#ifdef LUA_USE_MODULES_GPIO_PULSE
+  gpio_pulse_init(L);
+#endif
 #ifdef GPIO_INTERRUPT_ENABLE
   int i;
   for(i=0;i<GPIO_PIN_NUM;i++){

+ 431 - 0
app/modules/gpio_pulse.c

@@ -0,0 +1,431 @@
+#include "module.h"
+#include "lauxlib.h"
+#include "lmem.h"
+#include "platform.h"
+#include "user_interface.h"
+#include "c_types.h"
+#include "c_string.h"
+#include "gpio.h"
+#include "hw_timer.h"
+#include "pin_map.h"
+#include "driver/gpio16.h"
+
+#define TIMER_OWNER 'P'
+
+typedef struct {
+  uint32_t gpio_set;
+  uint32_t gpio_clr;
+  uint32_t delay;
+  uint32_t delay_min;
+  uint32_t delay_max;
+  uint32_t count;
+  uint32_t count_left;
+  uint16_t loop;
+} pulse_entry_t;
+
+typedef struct {
+  uint32_t entry_count;
+  volatile uint32_t steps;
+  volatile uint16_t entry_pos;
+  volatile int16_t stop_pos;  // -1 is stop nowhere, -2 is stop everywhere, otherwise is stop point
+  pulse_entry_t *entry;
+  volatile uint32_t expected_end_time;
+  volatile int32_t next_adjust;
+  volatile int cb_ref;
+} pulse_t;
+
+static int active_pulser_ref;
+static pulse_t *active_pulser;
+static task_handle_t tasknumber;
+
+static int gpio_pulse_push_state(lua_State *L, pulse_t *pulser) {
+  uint32_t now;
+  uint32_t expected_end_time;
+  uint32_t entry_pos;
+  uint32_t steps;
+  do {
+    now = 0x7FFFFFFF & system_get_time();
+    expected_end_time = pulser->expected_end_time;
+    entry_pos = pulser->entry_pos;
+    steps = pulser->steps;
+  } while (expected_end_time != pulser->expected_end_time ||
+           entry_pos != pulser->entry_pos ||
+           steps != pulser->steps);
+
+  if (entry_pos >= pulser->entry_count) {
+    lua_pushnil(L);
+  } else {
+    lua_pushinteger(L, entry_pos + 1);    // Lua is 1 offset
+  }
+  lua_pushinteger(L, steps);
+
+  int32_t diff = (expected_end_time - now) & 0x7fffffff;
+  lua_pushinteger(L, (diff << 1) >> 1);
+  lua_pushinteger(L, now);
+  return 4;
+}
+
+static int gpio_pulse_getstate(lua_State *L) {
+  pulse_t *pulser = luaL_checkudata(L, 1, "gpio.pulse");
+
+  if (pulser != active_pulser) {
+    return 0;
+  }
+
+  return gpio_pulse_push_state(L, pulser);
+}
+
+static int gpio_pulse_stop(lua_State *L) {
+  pulse_t *pulser = luaL_checkudata(L, 1, "gpio.pulse");
+
+  if (pulser != active_pulser) {
+    return 0;
+  }
+
+  int argno = 2;
+
+  int32_t stop_pos = -2;
+
+  if (lua_type(L, argno) == LUA_TNUMBER) {
+    stop_pos = luaL_checkinteger(L, 2);  
+    if (stop_pos != -2) {
+      if (stop_pos < 1 || stop_pos > pulser->entry_count) {
+        return luaL_error( L, "bad stop position: %d (valid range 1 - %d)", stop_pos, pulser->entry_count );
+      }
+      stop_pos = stop_pos - 1;
+    }
+    argno++;
+  }
+
+  if (lua_type(L, argno) == LUA_TFUNCTION || lua_type(L, argno) == LUA_TLIGHTFUNCTION) {
+    lua_pushvalue(L, argno);
+  } else {
+    return luaL_error( L, "missing callback" );
+  }
+
+  int new_cb_ref = luaL_ref(L, LUA_REGISTRYINDEX);
+  int cb_ref = pulser->cb_ref;
+
+  pulser->cb_ref = LUA_NOREF;
+  pulser->stop_pos = -1;
+  pulser->cb_ref = new_cb_ref;
+  pulser->stop_pos = stop_pos;
+
+  luaL_unref(L, LUA_REGISTRYINDEX, cb_ref);
+
+  lua_pushboolean(L, 1);
+  return 1;
+}
+
+static int gpio_pulse_delete(lua_State *L) {
+  pulse_t *pulser = luaL_checkudata(L, 1, "gpio.pulse");
+
+  if (pulser == active_pulser) {
+    return 0;
+  }
+
+  luaL_unref(L, LUA_REGISTRYINDEX, pulser->cb_ref);
+  return 0;
+}
+
+static int gpio_pulse_build(lua_State *L) {
+  // Take a table argument
+  luaL_checktype(L, 1, LUA_TTABLE);
+
+  // First figure out how big we need the block to be
+  size_t size = luaL_getn(L, 1);
+
+  if (size > 100) {
+    return luaL_error(L, "table is too large: %d entries is more than 100", size);
+  }
+
+  size_t memsize = sizeof(pulse_t) + size * sizeof(pulse_entry_t);
+  pulse_t *pulser = (pulse_t *) lua_newuserdata(L, memsize);
+  memset(pulser, 0, memsize);
+  //
+  // Associate its metatable
+  luaL_getmetatable(L, "gpio.pulse");
+  lua_setmetatable(L, -2);
+
+  pulser->entry = (pulse_entry_t *) (pulser + 1);
+  pulser->entry_count = size;
+
+  size_t i;
+  for (i = 0; i < size; i++) {
+    pulse_entry_t *entry = pulser->entry + i;
+
+    entry->delay_min = -1;
+    entry->delay_max = -1;
+
+    lua_rawgeti(L, 1, i + 1);
+
+    if (lua_type(L, -1) != LUA_TTABLE) {
+      return luaL_error(L, "All entries must be tables");
+    }
+
+    lua_pushnil(L);             // key position
+    while (lua_next(L, -2)) {
+      // stack now contains: -1 => value; -2 => key; -3 => table
+      if (lua_type(L, -2) == LUA_TNUMBER) {
+        int pin = lua_tonumber(L, -2);
+        int value = lua_tonumber(L, -1);
+
+        if (pin < 0 || pin >= GPIO_PIN_NUM) {
+          luaL_error(L, "pin number %d must be in range 0 .. %d", pin, GPIO_PIN_NUM - 1);
+        }
+
+        if (value) {
+          entry->gpio_set |= BIT(pin_num[pin]);
+        } else {
+          entry->gpio_clr |= BIT(pin_num[pin]);
+        }
+      } else {
+        const char *str = lua_tostring(L, -2);
+
+        if (strcmp(str, "delay") == 0) {
+          entry->delay = lua_tonumber(L, -1);
+          if (entry->delay < 0 || entry->delay > 1000000) {
+            return luaL_error(L, "delay of %d must be in the range 0 .. 1000000 microseconds", entry->delay);
+          }
+        } else if (strcmp(str, "min") == 0) {
+          entry->delay_min = lua_tonumber(L, -1);
+          if (entry->delay_min < 0 || entry->delay_min > 1000000) {
+            return luaL_error(L, "delay minimum of %d must be in the range 0 .. 1000000 microseconds", entry->delay_min);
+          }
+        } else if (strcmp(str, "max") == 0) {
+          entry->delay_max = lua_tonumber(L, -1);
+          if (entry->delay_max < 0 || entry->delay_max > 1000000) {
+            return luaL_error(L, "delay maximum of %d must be in the range 0 .. 1000000 microseconds", entry->delay_max);
+          }
+        } else if (strcmp(str, "count") == 0) {
+          entry->count = lua_tonumber(L, -1);
+        } else if (strcmp(str, "loop") == 0) {
+          entry->loop = lua_tonumber(L, -1);
+        } else {
+          return luaL_error(L, "Unrecognized key found: %s", str);
+        }
+      }
+      lua_pop(L, 1);
+    }
+
+    if (entry->delay_min != -1 || entry->delay_max != -1) {
+      if (entry->delay_min == -1) {
+        entry->delay_min = 0;
+      }
+      if (entry->delay_min > entry->delay ||
+          entry->delay_max < entry->delay) {
+        return luaL_error(L, "Delay of %d must be between min and max", entry->delay);
+      }
+    }
+
+    lua_pop(L, 1);
+  }
+
+  return 1;
+}
+
+static int gpio_pulse_adjust(lua_State *L) {
+  pulse_t *pulser = luaL_checkudata(L, 1, "gpio.pulse");
+
+  if (active_pulser != pulser) {
+    return 0;
+  }
+
+  int offset = luaL_checkinteger(L, 2);
+  // This will alter the next adjustable
+  pulser->next_adjust = offset;
+
+  int rc = gpio_pulse_push_state(L, active_pulser);
+
+  return rc;
+}
+
+static int gpio_pulse_cancel(lua_State *L) {
+  pulse_t *pulser = luaL_checkudata(L, 1, "gpio.pulse");
+
+  if (active_pulser != pulser) {
+    return 0;
+  }
+
+  // Shut off the timer
+  platform_hw_timer_close(TIMER_OWNER);
+
+  int rc = gpio_pulse_push_state(L, active_pulser);
+
+  active_pulser = NULL;
+
+  int pulser_ref = active_pulser_ref;
+  active_pulser_ref = LUA_NOREF;
+  luaL_unref(L, LUA_REGISTRYINDEX, pulser_ref);
+
+  return rc;
+}
+
+static void ICACHE_RAM_ATTR gpio_pulse_timeout(os_param_t p) {
+  (void) p;
+
+  int delay;
+
+  do {
+    if (!active_pulser || active_pulser->entry_pos >= active_pulser->entry_count) {
+      if (active_pulser) {
+        active_pulser->steps++;
+      }
+      platform_hw_timer_close(TIMER_OWNER);
+      task_post_low(tasknumber, (task_param_t)0);
+      return;
+    }
+    active_pulser->steps++;
+
+    pulse_entry_t *entry = active_pulser->entry + active_pulser->entry_pos;
+
+    // Yes, this means that there is more skew on D0 than on other pins....
+    if (entry->gpio_set & 0x10000) {
+      gpio16_output_set(1);
+    }
+    GPIO_REG_WRITE(GPIO_OUT_W1TS_ADDRESS, entry->gpio_set);
+    GPIO_REG_WRITE(GPIO_OUT_W1TC_ADDRESS, entry->gpio_clr);
+    if (entry->gpio_clr & 0x10000) {
+      gpio16_output_set(0);
+    }
+
+    int16_t stop = active_pulser->stop_pos;
+    if (stop == -2 || stop == active_pulser->entry_pos) {
+      platform_hw_timer_close(TIMER_OWNER);
+      task_post_low(tasknumber, (task_param_t)0);
+      return;
+    }
+
+    if (entry->loop) {
+      if (entry->count_left == 0) {
+        entry->count_left = entry->count + 1;
+      }
+      if (--entry->count_left >= 1) {
+        active_pulser->entry_pos = entry->loop - 1;       // zero offset
+      } else {
+        active_pulser->entry_pos++;
+      }
+    } else {
+      active_pulser->entry_pos++;
+    }
+
+    delay = entry->delay;
+
+    if (entry->delay_min != -1) {
+      int offset = active_pulser->next_adjust;
+      active_pulser->next_adjust = 0;
+      int delay_offset = 0x7fffffff & (system_get_time() - (active_pulser->expected_end_time + offset));
+      delay -= (delay_offset << 1) >> 1;
+      if (delay < entry->delay_min) {
+        active_pulser->next_adjust = entry->delay_min - delay;
+        delay = entry->delay_min;
+      } else if (delay > entry->delay_max) {
+        active_pulser->next_adjust = entry->delay_max - delay;
+        delay = entry->delay_max;
+      }
+    }
+
+    active_pulser->expected_end_time += delay;
+  } while (delay < 3);
+  
+  platform_hw_timer_arm_us(TIMER_OWNER, delay);
+}
+
+static int gpio_pulse_start(lua_State *L) {
+  pulse_t *pulser = luaL_checkudata(L, 1, "gpio.pulse");
+
+  if (active_pulser) {
+    return luaL_error(L, "pulse operation already in progress");
+  }
+
+  int argno = 2;
+  int initial_adjust;
+
+  if (lua_type(L, argno) == LUA_TNUMBER) {
+    initial_adjust = luaL_checkinteger(L, argno);
+    argno++;
+  }
+
+  if (lua_type(L, argno) == LUA_TFUNCTION || lua_type(L, argno) == LUA_TLIGHTFUNCTION) {
+    lua_pushvalue(L, argno);
+  } else {
+    return luaL_error( L, "missing callback" );
+  }
+
+  luaL_unref(L, LUA_REGISTRYINDEX, pulser->cb_ref);
+  pulser->cb_ref = luaL_ref(L, LUA_REGISTRYINDEX);
+
+  active_pulser = pulser;
+  lua_pushvalue(L, 1);
+  active_pulser_ref = luaL_ref(L, LUA_REGISTRYINDEX);
+
+  size_t i;
+  for (i = 0; i < pulser->entry_count; i++) {
+    pulser->entry[i].count_left = 0;
+  }
+  pulser->entry_pos = 0;
+  pulser->steps = 0;
+  pulser->stop_pos = -1;
+  pulser->next_adjust = initial_adjust;
+
+  // Now start things up
+  if (!platform_hw_timer_init(TIMER_OWNER, FRC1_SOURCE, TRUE)) {
+    // Failed to init the timer
+    luaL_error(L, "Unable to initialize timer");
+  }
+
+  active_pulser->expected_end_time = 0x7fffffff & system_get_time();
+  platform_hw_timer_set_func(TIMER_OWNER, gpio_pulse_timeout, 0);
+  gpio_pulse_timeout(0);
+
+  return 0;
+}
+
+static void gpio_pulse_task(os_param_t param, uint8_t prio) 
+{
+  (void) param;
+  (void) prio;
+
+  if (active_pulser) {
+    lua_State *L = lua_getstate();
+    // Invoke the callback
+    lua_rawgeti(L, LUA_REGISTRYINDEX, active_pulser->cb_ref);
+
+    int rc = gpio_pulse_push_state(L, active_pulser);
+
+    active_pulser = NULL;
+    int pulser_ref = active_pulser_ref;
+    active_pulser_ref = LUA_NOREF;
+    luaL_unref(L, LUA_REGISTRYINDEX, pulser_ref);
+
+    lua_call(L, rc, 0);
+  }
+}
+
+static const LUA_REG_TYPE pulse_map[] = {
+  { LSTRKEY( "getstate" ),            LFUNCVAL( gpio_pulse_getstate ) },
+  { LSTRKEY( "stop" ),                LFUNCVAL( gpio_pulse_stop ) },
+  { LSTRKEY( "cancel" ),              LFUNCVAL( gpio_pulse_cancel ) },
+  { LSTRKEY( "start" ),               LFUNCVAL( gpio_pulse_start ) },
+  { LSTRKEY( "adjust" ),              LFUNCVAL( gpio_pulse_adjust ) },
+  { LSTRKEY( "__gc" ),                LFUNCVAL( gpio_pulse_delete ) },
+  { LSTRKEY( "__index" ),             LROVAL( pulse_map ) },
+  { LNILKEY, LNILVAL }
+};
+
+const LUA_REG_TYPE gpio_pulse_map[] =
+{
+  { LSTRKEY( "build" ),               LFUNCVAL( gpio_pulse_build ) },
+  { LSTRKEY( "__index" ),             LROVAL( gpio_pulse_map ) },
+  { LNILKEY, LNILVAL }
+};
+
+int gpio_pulse_init(lua_State *L)
+{
+  luaL_rometatable(L, "gpio.pulse", (void *)pulse_map);
+  tasknumber = task_get_id(gpio_pulse_task);
+  return 0;
+}
+
+//NODEMCU_MODULE(GPIOPULSE, "gpiopulse", gpio_pulse_map, gpio_pulse_init);
+

+ 168 - 4
docs/en/modules/gpio.md

@@ -71,7 +71,7 @@ gpio.read(0)
 
 Serialize output based on a sequence of delay-times in µs. After each delay, the pin is toggled. After the last cycle and last delay the pin is not toggled.
 
-The function works in two modes: 
+The function works in two modes:
 * synchronous - for sub-50 µs resolution, restricted to max. overall duration,
 * asynchrounous - synchronous operation with less granularity but virtually unrestricted duration.
 
@@ -89,7 +89,7 @@ Note that the synchronous variant (no or nil `callback` parameter) function bloc
 #### Parameters
 - `pin`  pin to use, IO index
 - `start_level` level to start on, either `gpio.HIGH` or `gpio.LOW`
-- `delay_times` an array of delay times in µs between each toggle of the gpio pin. 
+- `delay_times` an array of delay times in µs between each toggle of the gpio pin.
 - `cycle_num` an optional number of times to run through the sequence. (default is 1)
 - `callback` an optional callback function or number, if present the function returns immediately and goes asynchronous.
 
@@ -122,8 +122,8 @@ This function is not available if GPIO_INTERRUPT_ENABLE was undefined at compile
 
 #### Parameters
 - `pin` **1-12**, pin to trigger on, IO index. Note that pin 0 does not support interrupts.
-- `type` "up", "down", "both", "low", "high", which represent *rising edge*, *falling edge*, *both 
-edges*, *low level*, and *high level* trigger modes respectivey. If the type is "none" or omitted 
+- `type` "up", "down", "both", "low", "high", which represent *rising edge*, *falling edge*, *both
+edges*, *low level*, and *high level* trigger modes respectivey. If the type is "none" or omitted
 then the callback function is removed and the interrupt is disabled.
 - `callback_function(level, when, eventcount)` callback function when trigger occurs. The level of the specified pin 
 at the interrupt passed as the first parameter to the callback. The timestamp of the event is passed
@@ -182,3 +182,167 @@ gpio.write(pin, gpio.HIGH)
 #### See also
 - [`gpio.mode()`](#gpiomode)
 - [`gpio.read()`](#gpioread)
+
+## gpio.pulse
+
+This covers a set of APIs that allow generation of pulse trains with accurate timing on
+multiple pins. It is similar to the `serout` API, but can handle multiple pins and has better
+timing control.
+
+The basic idea is to build a `gpio.pulse` object and then control it with methods on that object. Only one `gpio.pulse`
+object can be active at a time. The object is built from an array of tables where each inner table represents
+an action to take and the time to delay before moving to the next action.
+
+One of the uses for this is to generate bipolar impulse for driving clock movements where you want (say) a pulse on Pin 1 on the even
+second, and a pulse on Pin 2 on the odd second. `:getstate` and `:adjust` can be used to keep the pulse synchronized to the
+RTC clock (that is itself synchronized with NTP).
+
+!!! Configuration
+This feature is only available if `LUA_USE_MODULES_GPIO_PULSE` is defined at build time.
+
+## gpio.pulse.build
+
+This builds the `gpio.pulse` object from the supplied argument -- which is a table as described below.
+
+#### Syntax
+`gpio.pulse.build(table)`
+
+#### Parameter
+`table` this is view as an array of instructions. Each instruction is represented by a table as follows:
+
+- All numeric keys are considered to be pin numbers. The values of each are the value to be set onto the respective GPIO line. For example `{ [1] = gpio.HIGH }` would set pin 1 to be high. Note this that is the pin number and *not* the GPIO number. Multiple pins can be
+set at the same time.
+- `delay` specifies the number of microseconds after setting the pin values to wait until moving to the next state. The actual delay may be longer than this value depending on whether interrupts are enabled at the end time.
+- `min` and `max` can be used to specify (along with `delay`) that this time can be varied. If one time interval overruns, then the extra time will be deducted from a time period which has a `min` or `max` specified. The actual time can also be adjusted with the `:adjust` API below.
+- `count` and `loop` allow simple looping. When a state with `count` and `loop` is completed, the next state is at `loop` (provided that `count` has not decremented to zero). The first state is state 1.
+
+#### Returns
+`gpio.pulse` object.
+
+#### Example
+```lua
+gpio.mode(1,gpio.OUTPUT,gpio.PULLUP)
+gpio.mode(2,gpio.OUTPUT,gpio.PULLUP)
+
+pulser = gpio.pulse.build( {
+  { [1] = gpio.HIGH, [2] = gpio.LOW, delay=100000 },
+  { [1] = gpio.LOW, [2] = gpio.HIGH, delay=100000, loop=1, count=100, min=90000, max=110000 }
+})
+
+```
+
+
+## gpio.pulse:start
+
+This starts the output operations.
+
+#### Syntax
+`pulser:start([adjust, ] callback)`
+
+#### Parameter
+- `adjust` This is the number of microseconds to add to the next adjustable period. If this value is so large that
+it would push the delay past the `min` or `max`, then the remainder is held over until the next adjustable period.
+- `callback` This callback is executed when the pulses are complete. The callback is invoked with the same four
+parameters that are described as the return values of `gpio.pulse:getstate`.
+
+#### Returns
+`nil`
+
+#### Example
+```lua
+pulser:start(function(pos, steps, offset, now)
+             print (pos, steps, offset, now)
+             end)
+```
+
+## gpio.pulse:getstate
+
+This returns the current state. These four values are also passed into the callback functions.
+
+#### Syntax
+`pulser:getstate()`
+
+#### Returns
+
+- `position` is the index of the currently active state. The first state is state 1. This is `nil` if the output operation is complete.
+- `steps` is the number of states that have been executed (including the current one). This allows monitoring of progress when there are
+loops.
+- `offset` is the time (in microseconds) until the next state transition. This will be negative once the output operation is complete.
+- `now` is the value of the `tmr.now()` function at the instant when the `offset` was calculated.
+
+
+#### Example
+```lua
+pos, steps, offset, new = pulser:getstate()
+print (pos, steps, offset, now)
+```
+
+## gpio.pulse:stop
+
+This stops the output operation at some future time.
+
+#### Syntax
+`pulser:stop([position ,] callback)`
+
+#### Parameters
+
+- `position` is the index to stop at. The stopping happens on entry to this state. If not specified, then stops on the next state transition.
+- `callback` is invoked (with the same arguments as are returned by `:getstate`) when the operation has been stopped.
+
+#### Returns
+`true` if the stop will happen.
+
+
+#### Example
+```lua
+pulser:stop(function(pos, steps, offset, now)
+            print (pos, steps, offset, now)
+            end)
+```
+
+## gpio.pulse:cancel
+
+This stops the output operation immediately.
+
+#### Syntax
+`pulser:cancel()`
+
+#### Returns
+- `position` is the index of the currently active state. The first state is state 1. This is `nil` if the output operation is complete.
+- `steps` is the number of states that have been executed (including the current one). This allows monitoring of progress when there are
+loops.
+- `offset` is the time (in microseconds) until the next state transition. This will be negative once the output operation is complete.
+- `now` is the value of the `tmr.now()` function at the instant when the `offset` was calculated.
+
+
+#### Example
+```lua
+pulser:cancel(function(pos, steps, offset, now)
+              print (pos, steps, offset, now)
+              end)
+```
+
+## gpio.pulse:adjust
+
+This adds (or subtracts) time that will get used in the `min` / `max` delay case. This is useful if you are trying to
+synchronize a particular state to a particular time or external event.
+
+#### Syntax
+`pulser:adjust(offset)`
+
+#### Parameters
+- `offset` is the number of microseconds to be used in subsequent `min` / `max` delays. This overwrites any pending offset.
+
+#### Returns
+- `position` is the index of the currently active state. The first state is state 1. This is `nil` if the output operation is complete.
+- `steps` is the number of states that have been executed (including the current one). This allows monitoring of progress when there are
+loops.
+- `offset` is the time (in microseconds) until the next state transition. This will be negative once the output operation is complete.
+- `now` is the value of the `tmr.now()` function at the instant when the `offset` was calculated.
+
+
+#### Example
+```lua
+pulser:adjust(177)
+```
+