Add support for driving instrument gauge stepper motors (#1355)

This commit is contained in:
Philip Gladstone 2016-06-26 08:19:06 -04:00 committed by Marcel Stör
parent 4aad34158b
commit 4a8abc2060
6 changed files with 779 additions and 0 deletions

391
app/driver/switec.c Normal file
View File

@ -0,0 +1,391 @@
/*
* Module for interfacing with Switec instrument steppers (and
* similar devices). These are the steppers that are used in automotive
* instrument panels and the like. Run off 5 volts at low current.
*
* Code inspired by:
*
* SwitecX25 Arduino Library
* Guy Carpenter, Clearwater Software - 2012
*
* Licensed under the BSD2 license, see license.txt for details.
*
* NodeMcu integration by Philip Gladstone, N1DQ
*/
#include "platform.h"
#include "c_types.h"
#include "../libc/c_stdlib.h"
#include "../libc/c_stdio.h"
#include "driver/switec.h"
#include "ets_sys.h"
#include "os_type.h"
#include "osapi.h"
#include "hw_timer.h"
#include "user_interface.h"
#include "task/task.h"
#define N_STATES 6
//
// First pin passed to setup corresponds to bit 3
// On the motor, the pins are arranged
//
// 4 1
//
// 3 2
//
// The direction of rotation can be reversed by reordering the pins
//
// State 3 2 1 0 A B Value
// 0 1 0 0 1 - - 0x9
// 1 0 0 0 1 . - 0x1
// 2 0 1 1 1 + . 0x7
// 3 0 1 1 0 + + 0x6
// 4 1 1 1 0 . + 0xE
// 5 1 0 0 0 - . 0x8
static const uint8_t stateMap[N_STATES] = {0x9, 0x1, 0x7, 0x6, 0xE, 0x8};
typedef struct {
uint8_t current_state;
uint8_t stopped;
int8_t dir;
uint32_t mask;
uint32_t pinstate[N_STATES];
uint32_t next_time;
int16_t target_step;
int16_t current_step;
uint16_t vel;
uint16_t max_vel;
uint16_t min_delay;
task_handle_t task_number;
} DATA;
static DATA *data[SWITEC_CHANNEL_COUNT];
static volatile char timer_active;
#define MAXVEL 255
// Note that this has to be global so that the compiler does not
// put it into ROM.
uint8_t switec_accel_table[][2] = {
{ 20, 3000 >> 4},
{ 50, 1500 >> 4},
{ 100, 1000 >> 4},
{ 150, 800 >> 4},
{ MAXVEL, 600 >> 4}
};
static void ICACHE_RAM_ATTR timer_interrupt(os_param_t);
#define TIMER_OWNER ((os_param_t) 'S')
// Just takes the channel number
int switec_close(uint32_t channel)
{
if (channel >= sizeof(data) / sizeof(data[0])) {
return -1;
}
DATA *d = data[channel];
if (!d) {
return 0;
}
if (!d->stopped) {
return -1;
}
// Set pins as input
gpio_output_set(0, 0, 0, d->mask);
data[channel] = NULL;
c_free(d);
// See if there are any other channels active
for (channel = 0; channel < sizeof(data)/sizeof(data[0]); channel++) {
if (data[channel]) {
break;
}
}
// If not, then disable the interrupt
if (channel >= sizeof(data) / sizeof(data[0])) {
platform_hw_timer_close(TIMER_OWNER);
}
return 0;
}
static __attribute__((always_inline)) inline void write_io(DATA *d)
{
uint32_t pin_state = d->pinstate[d->current_state];
gpio_output_set(pin_state, d->mask & ~pin_state, 0, 0);
}
static __attribute__((always_inline)) inline void step_up(DATA *d)
{
d->current_step++;
d->current_state = (d->current_state + 1) % N_STATES;
write_io(d);
}
static __attribute__((always_inline)) inline void step_down(DATA *d)
{
d->current_step--;
d->current_state = (d->current_state + N_STATES - 1) % N_STATES;
write_io(d);
}
static void ICACHE_RAM_ATTR timer_interrupt(os_param_t p)
{
// This function really is running at interrupt level with everything
// else masked off. It should take as little time as necessary.
//
(void) p;
int i;
uint32_t delay = 0xffffffff;
// Loop over the channels to figure out which one needs action
for (i = 0; i < sizeof(data) / sizeof(data[0]); i++) {
DATA *d = data[i];
if (!d || d->stopped) {
continue;
}
uint32_t now = system_get_time();
if (now < d->next_time) {
int need_to_wait = d->next_time - now;
if (need_to_wait < delay) {
delay = need_to_wait;
}
continue;
}
// This channel is past it's action time. Need to process it
// Are we done yet?
if (d->current_step == d->target_step && d->vel == 0) {
d->stopped = 1;
d->dir = 0;
task_post_low(d->task_number, 0);
continue;
}
// if stopped, determine direction
if (d->vel == 0) {
d->dir = d->current_step < d->target_step ? 1 : -1;
// do not set to 0 or it could go negative in case 2 below
d->vel = 1;
}
// Move the pointer by one step in the correct direction
if (d->dir > 0) {
step_up(d);
} else {
step_down(d);
}
// determine delta, number of steps in current direction to target.
// may be negative if we are headed away from target
int delta = d->dir > 0 ? d->target_step - d->current_step : d->current_step - d->target_step;
if (delta > 0) {
// case 1 : moving towards target (maybe under accel or decel)
if (delta <= d->vel) {
// time to declerate
d->vel--;
} else if (d->vel < d->max_vel) {
// accelerating
d->vel++;
} else {
// at full speed - stay there
}
} else {
// case 2 : at or moving away from target (slow down!)
d->vel--;
}
// vel now defines delay
uint8_t row = 0;
// this is why vel must not be greater than the last vel in the table.
while (switec_accel_table[row][0] < d->vel) {
row++;
}
uint32_t micro_delay = switec_accel_table[row][1] << 4;
if (micro_delay < d->min_delay) {
micro_delay = d->min_delay;
}
// Figure out when we next need to take action
d->next_time = d->next_time + micro_delay;
if (d->next_time < now) {
d->next_time = now + micro_delay;
}
// Figure out how long to wait
int need_to_wait = d->next_time - now;
if (need_to_wait < delay) {
delay = need_to_wait;
}
}
if (delay < 1000000) {
if (delay < 50) {
delay = 50;
}
timer_active = 1;
platform_hw_timer_arm_us(TIMER_OWNER, delay);
} else {
timer_active = 0;
}
}
// The pin numbers are actual platform GPIO numbers
int switec_setup(uint32_t channel, int *pin, int max_deg_per_sec, task_handle_t task_number )
{
if (channel >= sizeof(data) / sizeof(data[0])) {
return -1;
}
if (data[channel]) {
if (switec_close(channel)) {
return -1;
}
}
DATA *d = (DATA *) c_zalloc(sizeof(DATA));
if (!d) {
return -1;
}
if (!data[0] && !data[1] && !data[2]) {
// We need to stup the timer as no channel was active before
// no autoreload
if (!platform_hw_timer_init(TIMER_OWNER, FRC1_SOURCE, FALSE)) {
// Failed to get the timer
c_free(d);
return -1;
}
}
data[channel] = d;
int i;
for (i = 0; i < 4; i++) {
// Build the mask for the pins to be output pins
d->mask |= 1 << pin[i];
int j;
// Now build the hi states for the pins according to the 6 phases above
for (j = 0; j < N_STATES; j++) {
if (stateMap[j] & (1 << (3 - i))) {
d->pinstate[j] |= 1 << pin[i];
}
}
}
d->max_vel = MAXVEL;
if (max_deg_per_sec == 0) {
max_deg_per_sec = 400;
}
d->min_delay = 1000000 / (3 * max_deg_per_sec);
d->task_number = task_number;
#ifdef SWITEC_DEBUG
for (i = 0; i < 4; i++) {
c_printf("pin[%d]=%d\n", i, pin[i]);
}
c_printf("Mask=0x%x\n", d->mask);
for (i = 0; i < N_STATES; i++) {
c_printf("pinstate[%d]=0x%x\n", i, d->pinstate[i]);
}
#endif
// Set all pins as outputs
gpio_output_set(0, 0, d->mask, 0);
platform_hw_timer_set_func(TIMER_OWNER, timer_interrupt, 0);
return 0;
}
// All this does is to assert that the current position is 0
int switec_reset(uint32_t channel)
{
if (channel >= sizeof(data) / sizeof(data[0])) {
return -1;
}
DATA *d = data[channel];
if (!d || !d->stopped) {
return -1;
}
d->current_step = d->target_step = 0;
return 0;
}
// Just takes the channel number and the position
int switec_moveto(uint32_t channel, int pos)
{
if (channel >= sizeof(data) / sizeof(data[0])) {
return -1;
}
DATA *d = data[channel];
if (!d) {
return -1;
}
if (pos < 0) {
// This ensures that we don't slam into the endstop
d->max_vel = 50;
} else {
d->max_vel = MAXVEL;
}
d->target_step = pos;
// If the pointer is not moving, setup so that we start it
if (d->stopped) {
// reset the timer to avoid possible time overflow giving spurious deltas
d->next_time = system_get_time() + 1000;
d->stopped = false;
if (!timer_active) {
timer_interrupt(0);
}
}
return 0;
}
// Get the current position, direction and target position
int switec_getpos(uint32_t channel, int32_t *pos, int32_t *dir, int32_t *target)
{
if (channel >= sizeof(data) / sizeof(data[0])) {
return -1;
}
DATA *d = data[channel];
if (!d) {
return -1;
}
*pos = d->current_step;
*dir = d->stopped ? 0 : d->dir;
*target = d->target_step;
return 0;
}

View File

@ -0,0 +1,21 @@
/*
* Definitions to access the Switec driver
*/
#ifndef __SWITEC_H__
#define __SWITEC_H__
#include "c_types.h"
#define SWITEC_CHANNEL_COUNT 3
int switec_setup(uint32_t channel, int *pin, int max_deg_per_sec, task_handle_t taskNumber );
int switec_close(uint32_t channel);
int switec_moveto(uint32_t channel, int pos);
int switec_reset(uint32_t channel);
int switec_getpos(uint32_t channel, int32_t *pos, int32_t *dir, int32_t *target);
#endif

View File

@ -54,6 +54,7 @@
//#define LUA_USE_MODULES_SNTP
#define LUA_USE_MODULES_SPI
//#define LUA_USE_MODULES_STRUCT
//#define LUA_USE_MODULES_SWITEC
//#define LUA_USE_MODULES_TM1829
#define LUA_USE_MODULES_TMR
//#define LUA_USE_MODULES_TSL2561

212
app/modules/switec.c Normal file
View File

@ -0,0 +1,212 @@
/*
* Module for interfacing with Switec instrument steppers (and
* similar devices). These are the steppers that are used in automotive
* instrument panels and the like. Run off 5 volts at low current.
*
* Code inspired by:
*
* SwitecX25 Arduino Library
* Guy Carpenter, Clearwater Software - 2012
*
* Licensed under the BSD2 license, see license.txt for details.
*
* NodeMcu integration by Philip Gladstone, N1DQ
*/
#include "module.h"
#include "lauxlib.h"
#include "platform.h"
#include "c_types.h"
#include "task/task.h"
#include "driver/switec.h"
// This is the reference to the callbacks for when the pointer
// stops moving.
static int stopped_callback[SWITEC_CHANNEL_COUNT] = { LUA_NOREF, LUA_NOREF, LUA_NOREF };
static task_handle_t tasknumber;
static void callback_free(lua_State* L, unsigned int id)
{
luaL_unref(L, LUA_REGISTRYINDEX, stopped_callback[id]);
stopped_callback[id] = LUA_NOREF;
}
static void callback_set(lua_State* L, unsigned int id, int argNumber)
{
if (lua_type(L, argNumber) == LUA_TFUNCTION || lua_type(L, argNumber) == LUA_TLIGHTFUNCTION) {
lua_pushvalue(L, argNumber); // copy argument (func) to the top of stack
callback_free(L, id);
stopped_callback[id] = luaL_ref(L, LUA_REGISTRYINDEX);
}
}
static void callback_execute(lua_State* L, unsigned int id)
{
if (stopped_callback[id] != LUA_NOREF) {
int callback = stopped_callback[id];
lua_rawgeti(L, LUA_REGISTRYINDEX, callback);
callback_free(L, id);
lua_call(L, 0, 0);
}
}
int platform_switec_exists( unsigned int id )
{
return (id < SWITEC_CHANNEL_COUNT);
}
// Lua: setup(id, P1, P2, P3, P4, maxSpeed)
static int lswitec_setup( lua_State* L )
{
unsigned int id;
id = luaL_checkinteger( L, 1 );
MOD_CHECK_ID( switec, id );
int pin[4];
if (switec_close(id)) {
return luaL_error( L, "Unable to setup stepper." );
}
int i;
for (i = 0; i < 4; i++) {
uint32_t gpio = luaL_checkinteger(L, 2 + i);
luaL_argcheck(L, platform_gpio_exists(gpio), 2 + i, "Invalid pin");
pin[i] = pin_num[gpio];
platform_gpio_mode(gpio, PLATFORM_GPIO_OUTPUT, PLATFORM_GPIO_PULLUP);
}
int deg_per_sec = 0;
if (lua_gettop(L) >= 6) {
deg_per_sec = luaL_checkinteger(L, 6);
}
if (switec_setup(id, pin, deg_per_sec, tasknumber)) {
return luaL_error(L, "Unable to setup stepper.");
}
return 0;
}
// Lua: close( id )
static int lswitec_close( lua_State* L )
{
unsigned int id;
id = luaL_checkinteger( L, 1 );
MOD_CHECK_ID( switec, id );
callback_free(L, id);
if (switec_close( id )) {
return luaL_error( L, "Unable to close stepper." );
}
return 0;
}
// Lua: reset( id )
static int lswitec_reset( lua_State* L )
{
unsigned int id;
id = luaL_checkinteger( L, 1 );
MOD_CHECK_ID( switec, id );
if (switec_reset( id )) {
return luaL_error( L, "Unable to reset stepper." );
}
return 0;
}
// Lua: moveto( id, pos [, cb] )
static int lswitec_moveto( lua_State* L )
{
unsigned int id;
id = luaL_checkinteger( L, 1 );
MOD_CHECK_ID( switec, id );
int pos;
pos = luaL_checkinteger( L, 2 );
if (lua_gettop(L) >= 3) {
callback_set(L, id, 3);
} else {
callback_free(L, id);
}
if (switec_moveto( id, pos )) {
return luaL_error( L, "Unable to move stepper." );
}
return 0;
}
// Lua: getpos( id ) -> position, moving
static int lswitec_getpos( lua_State* L )
{
unsigned int id;
id = luaL_checkinteger( L, 1 );
MOD_CHECK_ID( switec, id );
int32_t pos;
int32_t dir;
int32_t target;
if (switec_getpos( id, &pos, &dir, &target )) {
return luaL_error( L, "Unable to get position." );
}
lua_pushnumber(L, pos);
lua_pushnumber(L, dir);
return 2;
}
static int lswitec_dequeue(lua_State* L)
{
int id;
for (id = 0; id < SWITEC_CHANNEL_COUNT; id++) {
if (stopped_callback[id] != LUA_NOREF) {
int32_t pos;
int32_t dir;
int32_t target;
if (!switec_getpos( id, &pos, &dir, &target )) {
if (dir == 0 && pos == target) {
callback_execute(L, id);
}
}
}
}
return 0;
}
static void lswitec_task(os_param_t param, uint8_t prio)
{
(void) param;
(void) prio;
lswitec_dequeue(lua_getstate());
}
static int switec_open(lua_State *L)
{
(void) L;
tasknumber = task_get_id(lswitec_task);
return 0;
}
// Module function map
static const LUA_REG_TYPE switec_map[] = {
{ LSTRKEY( "setup" ), LFUNCVAL( lswitec_setup ) },
{ LSTRKEY( "close" ), LFUNCVAL( lswitec_close ) },
{ LSTRKEY( "reset" ), LFUNCVAL( lswitec_reset ) },
{ LSTRKEY( "moveto" ), LFUNCVAL( lswitec_moveto) },
{ LSTRKEY( "getpos" ), LFUNCVAL( lswitec_getpos) },
#ifdef SQITEC_DEBUG
{ LSTRKEY( "dequeue" ), LFUNCVAL( lswitec_dequeue) },
#endif
{ LNILKEY, LNILVAL }
};
NODEMCU_MODULE(SWITEC, "switec", switec_map, switec_open);

153
docs/en/modules/switec.md Normal file
View File

@ -0,0 +1,153 @@
# switec Module
This module controls a Switec X.27 (or compatible) instrument stepper motor. These are the
stepper motors that are used in modern automotive instrument clusters. They are incredibly cheap
and can be found at your favorite auction site or Chinese shopping site. There are varieties
which are dual axis -- i.e. have two stepper motors driving two concentric shafts so you
can mount two needles from the same axis.
These motors run off 5V (some may work off 3.3V). They draw under 20mA and are designed to be
driven directly from MCU pins. Since the nodemcu runs at 3.3V, a level translator is required.
An octal translator like the 74LVC4245A can perfom this translation. It also includes all the
protection diodes required.
These motors can be driven off three pins, with `pin2` and `pin3` being the same GPIO pin.
If the motor is directly connected to the MCU, then the current load is doubled and may exceed
the maximum ratings. If, however, a driver chip is being used, then the load on the MCU is neglible
and the same MCU pin can be connected to two driver pins. In order to do this, just specify
the same pin for `pin2` and `pin3`.
These motors do not have absolute positioning, but come with stops at both ends of the range.
The startup procedure is to drive the motor anti-clockwise until it is guaranteed that the needle
is on the step. Then this point can be set as zero. It is important not to let the motor
run into the endstops during normal operation as this will make the pointing inaccurate.
This module does not enforce any range limiting.
### Sources
These stepper motors are available at the following locations:
- Amazon: These are [X27 Stepper motors](http://www.amazon.com/s/ref=nb_sb_noss?url=search-alias%3Daps&field-keywords=instrument+stepper+motor+x27&rh=i%3Aaps%2Ck%3Ainstrument+stepper+motor+x27)
- Aliexpress: These are [X27 Stepper motors](http://www.aliexpress.com/wholesale?catId=0&initiative_id=SB_20160221132322&SearchText=x27+stepper). They also have the [Dual shaft version](http://www.aliexpress.com/wholesale?catId=0&initiative_id=SB_20160221132428&SearchText=vid28-05)
- Ebay: These are [X27 Stepper motors](http://www.ebay.com/sch/i.html?_from=R40&_trksid=p2050601.m570.l1313.TR0.TRC0.H0.Xx27+stepper+motor.TRS0&_nkw=x27+stepper+motor&_sacat=0)
##### Note
This module uses the hardware timer interrupt and hence it cannot be used at the same time as the PWM module.
Both modules can be compiled into the same firmware image, but an application can only use one. It may be
possible for an application to alternate between `switec` and `pwm`, but care must be taken.
## switec.setup()
Initialize the nodemcu to talk to a switec X.27 or compatible instrument stepper motor. The default
slew rate is set so that it should work for most motors. Some motors can run at 600 degress per second.
#### Syntax
`switec.setup(channel, pin1, pin2, pin3, pin4 [, maxDegPerSec])`
#### Parameters
- `channel` The switec module supports three stepper motors. The channel is either 0, 1 or 2.
- `pin1` This is a GPIO number and connects to pin 1 on the stepper.
- `pin2` This is a GPIO number and connects to pin 2 on the stepper.
- `pin3` This is a GPIO number and connects to pin 3 on the stepper.
- `pin4` This is a GPIO number and connects to pin 4 on the stepper.
- `maxDegPerSec` (optional) This can set to limit the maximum slew rate. The default is 400 degrees per second.
#### Returns
Nothing. If the arguments are in error, or the operation cannot be completed, then an error is thrown.
##### Note
Once a channel is setup, it cannot be re-setup until the needle has stopped moving.
#### Example
switec.setup(0, 5,6,7,8)
## switec.moveto()
Starts the needle moving to the specified position. If the needle is already moving, then the current
motion is cancelled, and the needle will move to the new position. It is possible to get a callback
when the needle stops moving. This is not normally required as multiple `moveto` operations can
be issued in quick succession. During the initial calibration, it is important. Note that the
callback is not guaranteed to be called -- it is possible that the needle never stops at the
target location before another `moveto` operation is triggered.
#### Syntax
`switec.moveto(channel, position[, stoppedCallback)`
#### Parameters
- `channel` The switec module supports three stepper motors. The channel is either 0, 1 or 2.
- `position` The position (number of steps clockwise) to move the needle. Typically in the range 0 to around 1000.
- `stoppedCallback` (optional) callback to be invoked when the needle stops moving.
#### Errors
The channel must have been setup, otherwise an error is thrown.
#### Example
switec.moveto(0, 1000, function ()
switec.moveto(0, 0)
end)
## switec.reset()
This sets the current position of the needle as being zero. The needle must be stationary.
#### Syntax
`switec.reset(channel)`
#### Parameters
- `channel` The switec module supports three stepper motors. The channel is either 0, 1 or 2.
#### Errors
The channel must have been setup and the needle must not be moving, otherwise an error is thrown.
#### Example
switec.reset(0)
## switec.getpos()
Gets the current position of the needle and whether it is moving.
#### Syntax
`switec.getpos(channel)`
#### Parameters
- `channel` The switec module supports three stepper motors. The channel is either 0, 1 or 2.
#### Returns
- `position` the current position of the needle
- `moving` 0 if the needle is stationary. 1 for clockwise, -1 for anti-clockwise.
#### Example
print switec.getpos(0)
## switec.close()
Releases the resources associated with the stepper.
#### Syntax
`switec.close(channel)`
#### Parameters
- `channel` The switec module supports three stepper motors. The channel is either 0, 1 or 2.
#### Errors
The needle must not be moving, otherwise an error is thrown.
#### Example
switec.close(0)
## Calibration
In order to set the zero point correctly, the needle should be driven anti-clockwise until
it runs into the end stop. Then the zero point can be set. The value of -1000 is used as that is
larger than the range of the motor -- i.e. it drives anti-clockwise through the entire range and
onto the end stop.
switec.setup(0, 5,6,7,8)
calibration = true
switec.moveto(0, -1000, function()
switec.reset(0)
calibration = false
end)
Other `moveto` operations should not be performed while `calibration` is set.

View File

@ -71,6 +71,7 @@ pages:
- 'sntp': 'en/modules/sntp.md'
- 'spi': 'en/modules/spi.md'
- 'struct': 'en/modules/struct.md'
- 'switec': 'en/modules/switec.md'
- 'tm1829': 'en/modules/tm1829.md'
- 'tmr': 'en/modules/tmr.md'
- 'tsl2561': 'en/modules/tsl2561.md'