Multiple button groups piping to HID

This commit is contained in:
Justin C. Miller
2025-02-23 17:21:09 -08:00
parent 3a8fa7d08f
commit eac47947cd
10 changed files with 364 additions and 149 deletions

View File

@@ -49,6 +49,7 @@ add_executable(edmfd)
target_sources(edmfd PUBLIC target_sources(edmfd PUBLIC
src/edmfd/blink.cc src/edmfd/blink.cc
src/edmfd/hid.cc src/edmfd/hid.cc
src/edmfd/i2c.cc
src/edmfd/main.cc src/edmfd/main.cc
src/edmfd/mcp23017.cc src/edmfd/mcp23017.cc
src/edmfd/screen.cc src/edmfd/screen.cc

View File

@@ -30,7 +30,8 @@ void set_pattern(pattern pat) {
board_led_write(led_state); board_led_write(led_state);
} }
void task(uint32_t time) { void task() {
uint32_t time = board_millis();
if (current_pattern == pattern::off || time < next_change) if (current_pattern == pattern::off || time < next_change)
return; return;
@@ -39,4 +40,4 @@ void task(uint32_t time) {
board_led_write(led_state); board_led_write(led_state);
} }
} // namespace blink } // namespace blink

View File

@@ -14,6 +14,6 @@ enum class pattern : uint8_t {
}; };
void set_pattern(pattern pat); void set_pattern(pattern pat);
void task(uint32_t time); void task();
} // namespace blink } // namespace blink

View File

@@ -12,8 +12,12 @@ extern "C" {
void tud_hid_set_report_cb(uint8_t, uint8_t, hid_report_type_t, uint8_t const*, uint16_t); void tud_hid_set_report_cb(uint8_t, uint8_t, hid_report_type_t, uint8_t const*, uint16_t);
} }
namespace hid { namespace hid {
static callback set_report_callback = nullptr;
void send_report(descriptor desc, uint32_t btn) void send_report(descriptor desc, uint32_t btn)
{ {
// skip if hid is not ready yet // skip if hid is not ready yet
@@ -48,30 +52,21 @@ void send_report(descriptor desc, uint32_t btn)
} }
} }
// Every 10ms, we will sent 1 report for each HID profile (keyboard, mouse etc ..) void init(callback cb)
// tud_hid_report_complete_cb() is used to send the next report after previous one is complete
void task(uint32_t time)
{ {
// Poll every 10ms set_report_callback = cb;
static constexpr uint32_t interval_ms = 10; }
static uint32_t next_update = 0;
if (time < next_update) return; // not enough time void task(uint32_t inputs)
next_update = time + interval_ms; {
// Remote wakeup if host is suspended. Requires host
uint32_t const btn = board_button_read(); // to have enabled REMOTE_WAKEUP feature for this device.
if (tud_suspended() && inputs) {
// Remote wakeup
if ( tud_suspended() && btn )
{
// Wake up host if we are in suspend mode
// and REMOTE_WAKEUP feature is enabled by host
tud_remote_wakeup(); tud_remote_wakeup();
}else return;
{
// Send the 1st of report chain, the rest will be sent by tud_hid_report_complete_cb()
send_report(descriptor::gamepad, btn);
} }
send_report(descriptor::gamepad, inputs);
} }
} // namespace hid } // namespace hid
@@ -93,23 +88,26 @@ void tud_hid_report_complete_cb(uint8_t instance, uint8_t const* report, uint16_
// Invoked when received GET_REPORT control request // Invoked when received GET_REPORT control request
// Application must fill buffer report's content and return its length. // Application must fill buffer report's content and return its length.
// Return zero will cause the stack to STALL request // Return zero will cause the stack to STALL request
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t reqlen) uint16_t tud_hid_get_report_cb(
uint8_t instance,
uint8_t report_id,
hid_report_type_t report_type,
uint8_t* buffer,
uint16_t reqlen)
{ {
// TODO not Implemented // TODO not Implemented
(void) instance;
(void) report_id;
(void) report_type;
(void) buffer;
(void) reqlen;
return 0; return 0;
} }
// Invoked when received SET_REPORT control request or // Invoked when received SET_REPORT control request or
// received data on OUT endpoint ( Report ID = 0, Type = 0 ) // received data on OUT endpoint ( Report ID = 0, Type = 0 )
void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t bufsize) void tud_hid_set_report_cb(
uint8_t instance,
uint8_t report_id,
hid_report_type_t report_type,
uint8_t const* buffer,
uint16_t bufsize)
{ {
(void) instance;
const auto desc = descriptor(report_id); const auto desc = descriptor(report_id);
if (report_type == HID_REPORT_TYPE_OUTPUT) { if (report_type == HID_REPORT_TYPE_OUTPUT) {
@@ -118,14 +116,12 @@ void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_
// bufsize should be (at least) 1 // bufsize should be (at least) 1
if ( bufsize < 1 ) return; if ( bufsize < 1 ) return;
uint8_t const btn_leds = buffer[0]; uint32_t btn_leds = 0;
if (btn_leds & 1) { for (unsigned i = 0; i < bufsize; ++i)
// Capslock On: disable blink, turn led on btn_leds = uint32_t(buffer[i]) << (i*8);
blink::set_pattern(blink::pattern::on);
} else { if (hid::set_report_callback)
// Caplocks Off: back to normal blink hid::set_report_callback(btn_leds);
blink::set_pattern(blink::pattern::mounted);
}
} }
} }
} }

View File

@@ -2,6 +2,9 @@
namespace hid { namespace hid {
void task(uint32_t time); using callback = void (*)(uint32_t);
} // namespace hid void init(callback cb);
void task(uint32_t inputs);
} // namespace hid

65
src/edmfd/i2c.cc Normal file
View File

@@ -0,0 +1,65 @@
#include <stdio.h>
#include "hardware/i2c.h"
#include "pico/stdlib.h"
#include "pico/binary_info.h"
#include "logging/log.hh"
#include "i2c.hh"
namespace {
// I2C reserves some addresses for special purposes. We exclude these from the scan.
// These are any addresses of the form 000 0xxx or 111 1xxx
bool reserved_addr(uint8_t addr) {
return (addr & 0x78) == 0 || (addr & 0x78) == 0x78;
}
}
namespace i2c {
void init(unsigned baud)
{
baud = i2c_init(i2c_default, baud);
gpio_set_function(PICO_DEFAULT_I2C_SDA_PIN, GPIO_FUNC_I2C);
gpio_set_function(PICO_DEFAULT_I2C_SCL_PIN, GPIO_FUNC_I2C);
gpio_pull_up(PICO_DEFAULT_I2C_SDA_PIN);
gpio_pull_up(PICO_DEFAULT_I2C_SCL_PIN);
// Make the I2C pins available to picotool
bi_decl(bi_2pins_with_func(PICO_DEFAULT_I2C_SDA_PIN, PICO_DEFAULT_I2C_SCL_PIN, GPIO_FUNC_I2C));
log::info("waiting for i2c...");
while(!i2c_get_write_available(i2c_default));
log::info("i2c initialized at %.1fkHz", baud/1000.f);
}
void scan()
{
printf("\nI2C Bus Scan\n");
printf(" 0 1 2 3 4 5 6 7 8 9 A B C D E F\n");
for (int addr = 0; addr < (1 << 7); ++addr) {
if (addr % 16 == 0) {
printf("%02x ", addr);
}
// Perform a 1-byte dummy read from the probe address. If a slave
// acknowledges this address, the function returns the number of bytes
// transferred. If the address byte is ignored, the function returns
// -1.
// Skip over any reserved addresses.
int ret;
uint8_t rxdata;
if (reserved_addr(addr))
ret = PICO_ERROR_GENERIC;
else
ret = i2c_read_blocking(i2c_default, addr, &rxdata, 1, false);
printf(ret < 0 ? "." : "@");
printf(addr % 16 == 15 ? "\n" : " ");
}
printf("Done.\n");
}
} // namespace i2c

8
src/edmfd/i2c.hh Normal file
View File

@@ -0,0 +1,8 @@
#pragma once
namespace i2c {
void init(unsigned baud);
void scan();
} // namespace i2c

View File

@@ -3,8 +3,6 @@
#include <string.h> #include <string.h>
#include "pico/stdlib.h" #include "pico/stdlib.h"
#include "pico/binary_info.h"
#include "hardware/i2c.h"
#include "tusb_config.h" #include "tusb_config.h"
#include "bsp/board_api.h" #include "bsp/board_api.h"
@@ -14,8 +12,13 @@
#include "blink.hh" #include "blink.hh"
#include "hid.hh" #include "hid.hh"
#include "i2c.hh"
#include "mcp23017.hh" #include "mcp23017.hh"
using mcp23017::reg;
static constexpr unsigned i2c_baud = 1200 * 1000;
extern "C" { extern "C" {
// TinyUSB callbacks // TinyUSB callbacks
void tud_mount_cb(); void tud_mount_cb();
@@ -24,73 +27,47 @@ extern "C" {
void tud_resume_cb(); void tud_resume_cb();
} }
// I2C reserves some addresses for special purposes. We exclude these from the scan. static constexpr unsigned bank_leds = 0;
// These are any addresses of the form 000 0xxx or 111 1xxx static constexpr unsigned bank_buttons = 1;
bool reserved_addr(uint8_t addr) {
return (addr & 0x78) == 0 || (addr & 0x78) == 0x78;
}
void setup_i2c() static constexpr unsigned hid_update_ms = 1000/20; // 20Hz
{
i2c_init(i2c_default, 100 * 1000);
gpio_set_function(PICO_DEFAULT_I2C_SDA_PIN, GPIO_FUNC_I2C);
gpio_set_function(PICO_DEFAULT_I2C_SCL_PIN, GPIO_FUNC_I2C);
gpio_pull_up(PICO_DEFAULT_I2C_SDA_PIN);
gpio_pull_up(PICO_DEFAULT_I2C_SCL_PIN);
// Make the I2C pins available to picotool static constexpr unsigned buttons_per_group = 5;
bi_decl(bi_2pins_with_func(PICO_DEFAULT_I2C_SDA_PIN, PICO_DEFAULT_I2C_SCL_PIN, GPIO_FUNC_I2C)); static constexpr unsigned button_group_count = 2;
} static const unsigned button_group_irqs[] = {5, 6};
mcp23017::extender button_groups[] ={ {0x20}, {0x21} };
uint8_t button_states[] = {0, 0};
uint8_t button_leds[] = {0, 0};
int write_mcp(uint8_t addr, uint8_t reg, uint8_t value, const char *regname) static_assert(sizeof(button_group_irqs) == button_group_count * sizeof(*button_group_irqs));
{ static_assert(sizeof(button_groups) == button_group_count * sizeof(*button_groups));
uint8_t txdata[2] = { reg, value };
int ret = i2c_write_blocking(i2c_default, addr, txdata, 2, false);
if (ret)
printf("Error %02x writing %02x to %02x:%s", ret, value, addr, regname);
else
printf("Wrote %02x to %02x:%s", value, addr, regname);
return ret; void irq_handler(unsigned pin, uint32_t events) {
} for (unsigned i = 0; i < button_group_count; ++i) {
if (button_group_irqs[i] != pin) continue;
void setup_mcp() button_states[i] = button_groups[i].get_gpios(bank_buttons);
{ break;
// A7 is button, B0 is LED }
static constexpr uint8_t addr = 0x20; }
if (write_mcp(addr, 0x00, 0xff, "IODIRA")) return;
if (write_mcp(addr, 0x10, 0x00, "IODIRB")) return; bool hid_update_callback(repeating_timer *timer) {
if (write_mcp(addr, 0x02, 0xff, "GPINTENA")) return; uint32_t buttons = 0;
if (write_mcp(addr, 0x06, 0xff, "GPPUA")) return; for (unsigned i = 0; i < button_group_count; ++i)
} buttons |= uint32_t(button_states[i]) << (i*8);
void i2c_scan() blink::task();
{ hid::task(buttons);
printf("\nI2C Bus Scan\n"); return true;
printf(" 0 1 2 3 4 5 6 7 8 9 A B C D E F\n"); }
for (int addr = 0; addr < (1 << 7); ++addr) { void set_leds_callback(uint32_t leds) {
if (addr % 16 == 0) { for (unsigned i = 0; i < button_group_count; ++i) {
printf("%02x ", addr); uint8_t desired = (leds >> (i*8)) & 0xff;
} if (desired != button_leds[i]) {
button_groups[i].set_gpios(bank_leds, desired);
// Perform a 1-byte dummy read from the probe address. If a slave button_leds[i] = desired;
// acknowledges this address, the function returns the number of bytes }
// transferred. If the address byte is ignored, the function returns
// -1.
// Skip over any reserved addresses.
int ret;
uint8_t rxdata;
if (reserved_addr(addr))
ret = PICO_ERROR_GENERIC;
else
ret = i2c_read_blocking(i2c_default, addr, &rxdata, 1, false);
printf(ret < 0 ? "." : "@");
printf(addr % 16 == 15 ? "\n" : " ");
} }
printf("Done.\n");
} }
int main(void) int main(void)
@@ -111,27 +88,50 @@ int main(void)
board_init_after_tusb(); board_init_after_tusb();
log::info("tinyusb initialized"); log::info("tinyusb initialized");
setup_i2c(); i2c::init(i2c_baud);
log::info("i2c initialized");
using mcp23017::reg; // set up interrupt handlers
mcp23017::gpios gpios {0x20}; for (int pin : button_group_irqs) {
gpio_init(pin);
gpio_pull_up(pin);
gpio_set_irq_enabled_with_callback(pin, GPIO_IRQ_EDGE_FALL, true, irq_handler);
}
uint8_t iocon = 0xba; bool have_any_buttons = false;
gpios.read(reg::iocona, &iocon); for (unsigned i = 0; i < button_group_count; ++i) {
gpios.read(reg::iodira, &iocon); if (!button_groups[i].init()) {
log::warn("failed to initialize button group %d", i);
continue;
}
log::info("initializing button group %d", i);
/* have_any_buttons = true;
gpios.write(reg::iodira, 0xff); mcp23017::extender &buttons = button_groups[i];
gpios.write(reg::gpintena, 0xff);
gpios.write(reg::gppua, 0xff);
*/
using mcp23017::direction;
for (unsigned i = 0; i < buttons_per_group; ++i)
buttons.set_direction(0, i, direction::out);
buttons.set_direction(1, 0, direction::in);
buttons.set_polarity(1, 0, true);
buttons.set_pullup(1, 0, true);
buttons.set_irq(1, 0, true);
}
if (!have_any_buttons) {
i2c::scan();
return 1;
}
hid::init(set_leds_callback);
repeating_timer hid_timer;
add_repeating_timer_ms(hid_update_ms, hid_update_callback, nullptr, &hid_timer);
// Let timers and iterrupts handle most things
while (1) { while (1) {
uint32_t time = board_millis();
tud_task(); // tinyusb device task tud_task(); // tinyusb device task
blink::task(time); sleep_ms(1);
hid::task(time);
} }
} }

View File

@@ -1,6 +1,7 @@
#include <stdio.h> #include <stdio.h>
#include "hardware/i2c.h" #include "hardware/i2c.h"
#include "pico/stdlib.h" #include "pico/stdlib.h"
#include "pico/time.h"
#include "logging/log.hh" #include "logging/log.hh"
#include "mcp23017.hh" #include "mcp23017.hh"
@@ -21,40 +22,158 @@ static const char * regnames[] = {
"olata", "olatb", "olata", "olatb",
}; };
gpios::gpios(uint8_t addr) : inline reg bank_reg(reg base, unsigned bank) { return reg(uint8_t(base)+bank); }
extender::extender(uint8_t addr) :
m_addr {addr} m_addr {addr}
{} {}
int bool
gpios::read(reg r, uint8_t *value) extender::init()
{ {
uint8_t txdata = uint8_t(r); unsigned count = 0;
int ret = i2c_write_burst_blocking(i2c_default, m_addr, &txdata, 1); while (1) {
if (ret < 0) { uint8_t value = 0xba;
log::error("writing read address {%02x:%s}: %d", m_addr, regnames[uint8_t(r)], ret); int ret = i2c_read_blocking(i2c_default, m_addr, &value, 1, false);
return ret; if (ret >= 0) break;
//log::error("reading %02x: %d", m_addr, ret);
if (count++ > 10) return false;
sleep_ms(10);
} }
ret = i2c_read_blocking(i2c_default, m_addr, value, 1, false); if (read(reg::iodira, m_direction, 2) > 0) {
if (ret < 0) { //log::info("initialized directions to %02x %02x", m_direction[0], m_direction[1]);
log::error("reading {%02x:%s}: %d", m_addr, regnames[uint8_t(r)], ret); return true;
} else {
log::info("read {%02x:%s}: 0x%02x", m_addr, regnames[uint8_t(r)], *value);
} }
return ret; return false;
}
template<reg base_reg, typename Value>
void extender::set_field(unsigned bank, unsigned pin, Value value, uint8_t (&current)[2])
{
if (bank > 1 || pin > 7) {
log::error("tried to configure pin %d:%d", bank, pin);
return;
}
Value current_value = Value((current[bank] >> pin) & 1);
if (value == current_value)
return;
uint8_t intval = uint8_t(value);
if (intval)
current[bank] |= 1 << pin;
else
current[bank] &= ~(1 << pin);
reg r = bank_reg(base_reg, bank);
write(r, current[bank]);
//log::info("configured bank %d %s: %02x", bank, regnames[int(r)], current[bank]);
}
void
extender::set_direction(unsigned bank, unsigned pin, direction dir)
{
set_field<reg::iodira>(bank, pin, dir, m_direction);
}
void
extender::set_polarity(unsigned bank, unsigned pin, bool reversed)
{
set_field<reg::ipola>(bank, pin, reversed, m_polarity);
}
void
extender::set_pullup(unsigned bank, unsigned pin, bool enabled)
{
set_field<reg::gppua>(bank, pin, enabled, m_pullup);
}
void
extender::set_irq(unsigned bank, unsigned pin, bool enabled)
{
set_field<reg::gpintena>(bank, pin, enabled, m_irq);
}
void
extender::set_gpio(unsigned bank, unsigned pin, bool on)
{
if (bank > 1 || pin > 7) {
log::error("tried to set pin %d:%d", bank, pin);
return;
}
uint8_t current = get_gpios(bank);
bool pin_current = (current >> pin) & 1;
if (on == pin_current)
return;
if (on)
current |= 1 << pin;
else
current &= ~(1 << pin);
set_gpios(bank, current);
}
void
extender::set_gpios(unsigned bank, uint8_t values)
{
if (bank > 1) {
log::error("tried to set bank %d", bank);
return;
}
reg r = bank_reg(reg::gpioa, bank);
int ret = write(r, values);
if (ret < 0) {
log::error("writing {%02x:%s}: %d", m_addr, regnames[uint8_t(r)], ret);
}
}
bool
extender::get_gpio(unsigned bank, unsigned pin)
{
if (bank > 1 || pin > 7) {
log::error("tried to get pin %d:%d", bank, pin);
return false;
}
return (get_gpios(bank) >> pin) & 1;
}
uint8_t
extender::get_gpios(unsigned bank)
{
if (bank > 1) {
log::error("tried to get bank %d", bank);
return 0;
}
uint8_t values = 0;
reg r = bank_reg(reg::gpioa, bank);
int ret = read(r, &values, 1);
if (ret < 0)
log::error("reading {%02x:%s}: %d", m_addr, regnames[uint8_t(r)], ret);
return values;
} }
int int
gpios::write(reg r, uint8_t value) extender::read(reg r, uint8_t *values, unsigned count)
{
uint8_t txdata = uint8_t(r);
int ret = i2c_write_blocking(i2c_default, m_addr, &txdata, 1, true);
if (ret < 0)
return ret;
return i2c_read_blocking(i2c_default, m_addr, values, count, false);
}
int
extender::write(reg r, uint8_t value)
{ {
uint8_t txdata[2] = { uint8_t(r), value }; uint8_t txdata[2] = { uint8_t(r), value };
int ret = i2c_write_blocking(i2c_default, m_addr, txdata, sizeof(txdata), false); return i2c_write_blocking(i2c_default, m_addr, txdata, sizeof(txdata), false);
if (ret != PICO_OK) {
log::error("writing {%02x:%s}: %d", m_addr, regnames[uint8_t(r)], ret);
} else {
log::info("wrote {%02x:%s}: 0x%02x", m_addr, regnames[uint8_t(r)], value);
}
return ret;
} }
} // namespace mcp23017 } // namespace mcp23017

View File

@@ -16,16 +16,38 @@ enum class reg : uint8_t {
olata, olatb, olata, olatb,
}; };
class gpios enum class direction { out, in };
class extender
{ {
public: public:
gpios(uint8_t addr); extender(uint8_t addr);
int read(reg r, uint8_t *value); bool init();
int write(reg r, uint8_t value);
void set_direction(unsigned bank, unsigned pin, direction dir);
void set_polarity(unsigned bank, unsigned pin, bool reversed);
void set_pullup(unsigned bank, unsigned pin, bool enabled);
void set_irq(unsigned bank, unsigned pin, bool enabled);
void set_gpio(unsigned bank, unsigned pin, bool on);
void set_gpios(unsigned bank, uint8_t values);
bool get_gpio(unsigned bank, unsigned pin);
uint8_t get_gpios(unsigned bank);
private: private:
template<reg base_reg, typename Value>
void set_field(unsigned bank, unsigned pin, Value value, uint8_t (&current)[2]);
int write(reg r, uint8_t value);
int read(reg r, uint8_t *value, unsigned count);
uint8_t m_addr; uint8_t m_addr;
uint8_t m_direction[2];
uint8_t m_polarity[2];
uint8_t m_pullup[2];
uint8_t m_irq[2];
}; };
} // namespace mcp23017 } // namespace mcp23017