322 lines
7.4 KiB
C
322 lines
7.4 KiB
C
/*
|
|
* Driver for interfacing to cheap rotary switches that
|
|
* have a quadrature output with an optional press button
|
|
*
|
|
* This sets up the relevant gpio as interrupt and then keeps track of
|
|
* the position of the switch in software. Changes are enqueued to task
|
|
* level and a task message posted when required. If the queue fills up
|
|
* then moves are ignored, but the last press/release will be included.
|
|
*
|
|
* Philip Gladstone, N1DQ
|
|
*/
|
|
|
|
#include "platform.h"
|
|
#include "c_types.h"
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include "driver/rotary.h"
|
|
#include "user_interface.h"
|
|
#include "esp_system.h"
|
|
#include "task/task.h"
|
|
#include "ets_sys.h"
|
|
|
|
//
|
|
// Queue is empty if read == write.
|
|
// However, we always want to keep the previous value
|
|
// so writing is only allowed if write - read < QUEUE_SIZE - 1
|
|
|
|
#define QUEUE_SIZE 8
|
|
|
|
#define GET_LAST_STATUS(d) (d->queue[(d->write_offset-1) & (QUEUE_SIZE - 1)])
|
|
#define GET_PREV_STATUS(d) (d->queue[(d->write_offset-2) & (QUEUE_SIZE - 1)])
|
|
#define HAS_QUEUED_DATA(d) (d->read_offset < d->write_offset)
|
|
#define HAS_QUEUE_SPACE(d) (d->read_offset + QUEUE_SIZE - 1 > d->write_offset)
|
|
|
|
#define REPLACE_STATUS(d, x) (d->queue[(d->write_offset-1) & (QUEUE_SIZE - 1)] = (rotary_event_t) { (x), system_get_time() })
|
|
#define QUEUE_STATUS(d, x) (d->queue[(d->write_offset++) & (QUEUE_SIZE - 1)] = (rotary_event_t) { (x), system_get_time() })
|
|
|
|
#define GET_READ_STATUS(d) (d->queue[d->read_offset & (QUEUE_SIZE - 1)])
|
|
#define ADVANCE_IF_POSSIBLE(d) if (d->read_offset < d->write_offset) { d->read_offset++; }
|
|
|
|
#define STATUS_IS_PRESSED(x) ((x & 0x80000000) != 0)
|
|
|
|
typedef struct {
|
|
int8_t phase_a_pin;
|
|
int8_t phase_b_pin;
|
|
int8_t press_pin;
|
|
uint32_t read_offset; // Accessed by task
|
|
uint32_t write_offset; // Accessed by ISR
|
|
uint32_t pin_mask;
|
|
uint32_t phase_a;
|
|
uint32_t phase_b;
|
|
uint32_t press;
|
|
uint32_t last_press_change_time;
|
|
int tasknumber;
|
|
rotary_event_t queue[QUEUE_SIZE];
|
|
} DATA;
|
|
|
|
static DATA *data[ROTARY_CHANNEL_COUNT];
|
|
|
|
static uint8_t task_queued;
|
|
|
|
static void set_gpio_bits(void);
|
|
|
|
static void rotary_clear_pin(int pin)
|
|
{
|
|
if (pin >= 0) {
|
|
gpio_pin_intr_state_set(GPIO_ID_PIN(pin_num[pin]), GPIO_PIN_INTR_DISABLE);
|
|
platform_gpio_mode(pin, PLATFORM_GPIO_INPUT, PLATFORM_GPIO_PULLUP);
|
|
}
|
|
}
|
|
|
|
// Just takes the channel number. Cleans up the resources used.
|
|
int rotary_close(uint32_t channel)
|
|
{
|
|
if (channel >= sizeof(data) / sizeof(data[0])) {
|
|
return -1;
|
|
}
|
|
|
|
DATA *d = data[channel];
|
|
|
|
if (!d) {
|
|
return 0;
|
|
}
|
|
|
|
data[channel] = NULL;
|
|
|
|
rotary_clear_pin(d->phase_a_pin);
|
|
rotary_clear_pin(d->phase_b_pin);
|
|
rotary_clear_pin(d->press_pin);
|
|
|
|
free(d);
|
|
|
|
set_gpio_bits();
|
|
|
|
return 0;
|
|
}
|
|
|
|
static uint32_t ICACHE_RAM_ATTR rotary_interrupt(uint32_t ret_gpio_status)
|
|
{
|
|
// This function really is running at interrupt level with everything
|
|
// else masked off. It should take as little time as necessary.
|
|
//
|
|
//
|
|
|
|
// This gets the set of pins which have changed status
|
|
uint32 gpio_status = GPIO_REG_READ(GPIO_STATUS_ADDRESS);
|
|
|
|
int i;
|
|
for (i = 0; i < sizeof(data) / sizeof(data[0]); i++) {
|
|
DATA *d = data[i];
|
|
if (!d || (gpio_status & d->pin_mask) == 0) {
|
|
continue;
|
|
}
|
|
|
|
GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, gpio_status & d->pin_mask);
|
|
|
|
uint32_t bits = GPIO_REG_READ(GPIO_IN_ADDRESS);
|
|
|
|
uint32_t last_status = GET_LAST_STATUS(d).pos;
|
|
|
|
uint32_t now = system_get_time();
|
|
|
|
uint32_t new_status;
|
|
|
|
new_status = last_status & 0x80000000;
|
|
|
|
// This is the debounce logic for the press switch. We ignore changes
|
|
// for 10ms after a change.
|
|
if (now - d->last_press_change_time > 10 * 1000) {
|
|
new_status = (bits & d->press) ? 0 : 0x80000000;
|
|
if (STATUS_IS_PRESSED(new_status ^ last_status)) {
|
|
d->last_press_change_time = now;
|
|
}
|
|
}
|
|
|
|
// A B
|
|
// 1 1 => 0
|
|
// 1 0 => 1
|
|
// 0 0 => 2
|
|
// 0 1 => 3
|
|
|
|
int micropos = 2;
|
|
if (bits & d->phase_b) {
|
|
micropos = 3;
|
|
}
|
|
if (bits & d->phase_a) {
|
|
micropos ^= 3;
|
|
}
|
|
|
|
int32_t rotary_pos = last_status;
|
|
|
|
switch ((micropos - last_status) & 3) {
|
|
case 0:
|
|
// No change, nothing to do
|
|
break;
|
|
case 1:
|
|
// Incremented by 1
|
|
rotary_pos++;
|
|
break;
|
|
case 3:
|
|
// Decremented by 1
|
|
rotary_pos--;
|
|
break;
|
|
default:
|
|
// We missed an interrupt
|
|
// We will ignore... but mark it.
|
|
rotary_pos += 1000000;
|
|
break;
|
|
}
|
|
|
|
new_status |= rotary_pos & 0x7fffffff;
|
|
|
|
if (last_status != new_status) {
|
|
// Either we overwrite the status or we add a new one
|
|
if (!HAS_QUEUED_DATA(d)
|
|
|| STATUS_IS_PRESSED(last_status ^ new_status)
|
|
|| STATUS_IS_PRESSED(last_status ^ GET_PREV_STATUS(d).pos)) {
|
|
if (HAS_QUEUE_SPACE(d)) {
|
|
QUEUE_STATUS(d, new_status);
|
|
if (!task_queued) {
|
|
if (task_post_medium(d->tasknumber, (task_param_t) &task_queued)) {
|
|
task_queued = 1;
|
|
}
|
|
}
|
|
} else {
|
|
REPLACE_STATUS(d, new_status);
|
|
}
|
|
} else {
|
|
REPLACE_STATUS(d, new_status);
|
|
}
|
|
}
|
|
ret_gpio_status &= ~(d->pin_mask);
|
|
}
|
|
|
|
return ret_gpio_status;
|
|
}
|
|
|
|
// The pin numbers are actual platform GPIO numbers
|
|
int rotary_setup(uint32_t channel, int phase_a, int phase_b, int press, task_handle_t tasknumber )
|
|
{
|
|
if (channel >= sizeof(data) / sizeof(data[0])) {
|
|
return -1;
|
|
}
|
|
|
|
if (data[channel]) {
|
|
if (rotary_close(channel)) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
DATA *d = (DATA *) zalloc(sizeof(DATA));
|
|
if (!d) {
|
|
return -1;
|
|
}
|
|
|
|
data[channel] = d;
|
|
int i;
|
|
|
|
d->tasknumber = tasknumber;
|
|
|
|
d->phase_a = 1 << pin_num[phase_a];
|
|
platform_gpio_mode(phase_a, PLATFORM_GPIO_INT, PLATFORM_GPIO_PULLUP);
|
|
gpio_pin_intr_state_set(GPIO_ID_PIN(pin_num[phase_a]), GPIO_PIN_INTR_ANYEDGE);
|
|
d->phase_a_pin = phase_a;
|
|
|
|
d->phase_b = 1 << pin_num[phase_b];
|
|
platform_gpio_mode(phase_b, PLATFORM_GPIO_INT, PLATFORM_GPIO_PULLUP);
|
|
gpio_pin_intr_state_set(GPIO_ID_PIN(pin_num[phase_b]), GPIO_PIN_INTR_ANYEDGE);
|
|
d->phase_b_pin = phase_b;
|
|
|
|
if (press >= 0) {
|
|
d->press = 1 << pin_num[press];
|
|
platform_gpio_mode(press, PLATFORM_GPIO_INT, PLATFORM_GPIO_PULLUP);
|
|
gpio_pin_intr_state_set(GPIO_ID_PIN(pin_num[press]), GPIO_PIN_INTR_ANYEDGE);
|
|
}
|
|
d->press_pin = press;
|
|
|
|
d->pin_mask = d->phase_a | d->phase_b | d->press;
|
|
|
|
set_gpio_bits();
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void set_gpio_bits()
|
|
{
|
|
uint32_t bits = 0;
|
|
for (int i = 0; i < ROTARY_CHANNEL_COUNT; i++) {
|
|
DATA *d = data[i];
|
|
|
|
if (d) {
|
|
bits = bits | d->pin_mask;
|
|
}
|
|
}
|
|
|
|
platform_gpio_register_intr_hook(bits, rotary_interrupt);
|
|
}
|
|
|
|
bool rotary_has_queued_event(uint32_t channel)
|
|
{
|
|
if (channel >= sizeof(data) / sizeof(data[0])) {
|
|
return FALSE;
|
|
}
|
|
|
|
DATA *d = data[channel];
|
|
|
|
if (!d) {
|
|
return FALSE;
|
|
}
|
|
|
|
return HAS_QUEUED_DATA(d);
|
|
}
|
|
|
|
// Get the oldest event in the queue and remove it (if possible)
|
|
bool rotary_getevent(uint32_t channel, rotary_event_t *resultp)
|
|
{
|
|
rotary_event_t result = { 0 };
|
|
|
|
if (channel >= sizeof(data) / sizeof(data[0])) {
|
|
return FALSE;
|
|
}
|
|
|
|
DATA *d = data[channel];
|
|
|
|
if (!d) {
|
|
return FALSE;
|
|
}
|
|
|
|
ETS_GPIO_INTR_DISABLE();
|
|
|
|
bool status = FALSE;
|
|
|
|
if (HAS_QUEUED_DATA(d)) {
|
|
result = GET_READ_STATUS(d);
|
|
d->read_offset++;
|
|
status = TRUE;
|
|
} else {
|
|
result = GET_LAST_STATUS(d);
|
|
}
|
|
|
|
ETS_GPIO_INTR_ENABLE();
|
|
|
|
*resultp = result;
|
|
|
|
return status;
|
|
}
|
|
|
|
int rotary_getpos(uint32_t channel)
|
|
{
|
|
if (channel >= sizeof(data) / sizeof(data[0])) {
|
|
return -1;
|
|
}
|
|
|
|
DATA *d = data[channel];
|
|
|
|
if (!d) {
|
|
return -1;
|
|
}
|
|
|
|
return GET_LAST_STATUS(d).pos;
|
|
}
|