From eac47947cd36b77129dd90476f7239232a7b6ed7 Mon Sep 17 00:00:00 2001 From: "Justin C. Miller" Date: Sun, 23 Feb 2025 17:21:09 -0800 Subject: [PATCH] Multiple button groups piping to HID --- CMakeLists.txt | 1 + src/edmfd/blink.cc | 5 +- src/edmfd/blink.hh | 4 +- src/edmfd/hid.cc | 72 +++++++++---------- src/edmfd/hid.hh | 7 +- src/edmfd/i2c.cc | 65 +++++++++++++++++ src/edmfd/i2c.hh | 8 +++ src/edmfd/main.cc | 158 ++++++++++++++++++++-------------------- src/edmfd/mcp23017.cc | 163 ++++++++++++++++++++++++++++++++++++------ src/edmfd/mcp23017.hh | 30 ++++++-- 10 files changed, 364 insertions(+), 149 deletions(-) create mode 100644 src/edmfd/i2c.cc create mode 100644 src/edmfd/i2c.hh diff --git a/CMakeLists.txt b/CMakeLists.txt index a34e098..21781b6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,6 +49,7 @@ add_executable(edmfd) target_sources(edmfd PUBLIC src/edmfd/blink.cc src/edmfd/hid.cc + src/edmfd/i2c.cc src/edmfd/main.cc src/edmfd/mcp23017.cc src/edmfd/screen.cc diff --git a/src/edmfd/blink.cc b/src/edmfd/blink.cc index 87f6dc7..57abcc5 100644 --- a/src/edmfd/blink.cc +++ b/src/edmfd/blink.cc @@ -30,7 +30,8 @@ void set_pattern(pattern pat) { 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) return; @@ -39,4 +40,4 @@ void task(uint32_t time) { board_led_write(led_state); } -} // namespace blink \ No newline at end of file +} // namespace blink diff --git a/src/edmfd/blink.hh b/src/edmfd/blink.hh index 762cf2a..99d7a7f 100644 --- a/src/edmfd/blink.hh +++ b/src/edmfd/blink.hh @@ -14,6 +14,6 @@ enum class pattern : uint8_t { }; void set_pattern(pattern pat); -void task(uint32_t time); +void task(); -} // namespace blink \ No newline at end of file +} // namespace blink diff --git a/src/edmfd/hid.cc b/src/edmfd/hid.cc index 7656676..71a9996 100644 --- a/src/edmfd/hid.cc +++ b/src/edmfd/hid.cc @@ -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); } + namespace hid { +static callback set_report_callback = nullptr; + + void send_report(descriptor desc, uint32_t btn) { // 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 ..) -// tud_hid_report_complete_cb() is used to send the next report after previous one is complete -void task(uint32_t time) +void init(callback cb) { - // Poll every 10ms - static constexpr uint32_t interval_ms = 10; - static uint32_t next_update = 0; + set_report_callback = cb; +} - if (time < next_update) return; // not enough time - next_update = time + interval_ms; - - uint32_t const btn = board_button_read(); - - // Remote wakeup - if ( tud_suspended() && btn ) - { - // Wake up host if we are in suspend mode - // and REMOTE_WAKEUP feature is enabled by host +void task(uint32_t inputs) +{ + // Remote wakeup if host is suspended. Requires host + // to have enabled REMOTE_WAKEUP feature for this device. + if (tud_suspended() && inputs) { tud_remote_wakeup(); - }else - { - // Send the 1st of report chain, the rest will be sent by tud_hid_report_complete_cb() - send_report(descriptor::gamepad, btn); + return; } + + send_report(descriptor::gamepad, inputs); } } // 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 // Application must fill buffer report's content and return its length. // 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 - (void) instance; - (void) report_id; - (void) report_type; - (void) buffer; - (void) reqlen; - return 0; } // Invoked when received SET_REPORT control request or // 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); 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 if ( bufsize < 1 ) return; - uint8_t const btn_leds = buffer[0]; - if (btn_leds & 1) { - // Capslock On: disable blink, turn led on - blink::set_pattern(blink::pattern::on); - } else { - // Caplocks Off: back to normal blink - blink::set_pattern(blink::pattern::mounted); - } + uint32_t btn_leds = 0; + for (unsigned i = 0; i < bufsize; ++i) + btn_leds = uint32_t(buffer[i]) << (i*8); + + if (hid::set_report_callback) + hid::set_report_callback(btn_leds); } } -} \ No newline at end of file +} diff --git a/src/edmfd/hid.hh b/src/edmfd/hid.hh index 0b1c3a5..c901907 100644 --- a/src/edmfd/hid.hh +++ b/src/edmfd/hid.hh @@ -2,6 +2,9 @@ namespace hid { -void task(uint32_t time); +using callback = void (*)(uint32_t); -} // namespace hid \ No newline at end of file +void init(callback cb); +void task(uint32_t inputs); + +} // namespace hid diff --git a/src/edmfd/i2c.cc b/src/edmfd/i2c.cc new file mode 100644 index 0000000..06c1599 --- /dev/null +++ b/src/edmfd/i2c.cc @@ -0,0 +1,65 @@ +#include +#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 diff --git a/src/edmfd/i2c.hh b/src/edmfd/i2c.hh new file mode 100644 index 0000000..7ae03d5 --- /dev/null +++ b/src/edmfd/i2c.hh @@ -0,0 +1,8 @@ +#pragma once + +namespace i2c { + +void init(unsigned baud); +void scan(); + +} // namespace i2c diff --git a/src/edmfd/main.cc b/src/edmfd/main.cc index 3fdb51a..91dc977 100644 --- a/src/edmfd/main.cc +++ b/src/edmfd/main.cc @@ -3,8 +3,6 @@ #include #include "pico/stdlib.h" -#include "pico/binary_info.h" -#include "hardware/i2c.h" #include "tusb_config.h" #include "bsp/board_api.h" @@ -14,8 +12,13 @@ #include "blink.hh" #include "hid.hh" +#include "i2c.hh" #include "mcp23017.hh" +using mcp23017::reg; + +static constexpr unsigned i2c_baud = 1200 * 1000; + extern "C" { // TinyUSB callbacks void tud_mount_cb(); @@ -24,73 +27,47 @@ extern "C" { void tud_resume_cb(); } -// 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; -} +static constexpr unsigned bank_leds = 0; +static constexpr unsigned bank_buttons = 1; -void setup_i2c() -{ - 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); +static constexpr unsigned hid_update_ms = 1000/20; // 20Hz - // 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)); -} +static constexpr unsigned buttons_per_group = 5; +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) -{ - 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); +static_assert(sizeof(button_group_irqs) == button_group_count * sizeof(*button_group_irqs)); +static_assert(sizeof(button_groups) == button_group_count * sizeof(*button_groups)); - return ret; -} - -void setup_mcp() -{ - // 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; - if (write_mcp(addr, 0x02, 0xff, "GPINTENA")) return; - if (write_mcp(addr, 0x06, 0xff, "GPPUA")) return; -} - -void i2c_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" : " "); +void irq_handler(unsigned pin, uint32_t events) { + for (unsigned i = 0; i < button_group_count; ++i) { + if (button_group_irqs[i] != pin) continue; + button_states[i] = button_groups[i].get_gpios(bank_buttons); + break; + } +} + +bool hid_update_callback(repeating_timer *timer) { + uint32_t buttons = 0; + for (unsigned i = 0; i < button_group_count; ++i) + buttons |= uint32_t(button_states[i]) << (i*8); + + blink::task(); + hid::task(buttons); + return true; +} + +void set_leds_callback(uint32_t leds) { + for (unsigned i = 0; i < button_group_count; ++i) { + uint8_t desired = (leds >> (i*8)) & 0xff; + if (desired != button_leds[i]) { + button_groups[i].set_gpios(bank_leds, desired); + button_leds[i] = desired; + } } - printf("Done.\n"); } int main(void) @@ -111,27 +88,50 @@ int main(void) board_init_after_tusb(); log::info("tinyusb initialized"); - setup_i2c(); - log::info("i2c initialized"); + i2c::init(i2c_baud); - using mcp23017::reg; - mcp23017::gpios gpios {0x20}; + // set up interrupt handlers + 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; - gpios.read(reg::iocona, &iocon); - gpios.read(reg::iodira, &iocon); + bool have_any_buttons = false; + for (unsigned i = 0; i < button_group_count; ++i) { + if (!button_groups[i].init()) { + log::warn("failed to initialize button group %d", i); + continue; + } + log::info("initializing button group %d", i); - /* - gpios.write(reg::iodira, 0xff); - gpios.write(reg::gpintena, 0xff); - gpios.write(reg::gppua, 0xff); - */ + have_any_buttons = true; + mcp23017::extender &buttons = button_groups[i]; + 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) { - uint32_t time = board_millis(); tud_task(); // tinyusb device task - blink::task(time); - hid::task(time); + sleep_ms(1); } } diff --git a/src/edmfd/mcp23017.cc b/src/edmfd/mcp23017.cc index e210464..2633d62 100644 --- a/src/edmfd/mcp23017.cc +++ b/src/edmfd/mcp23017.cc @@ -1,6 +1,7 @@ #include #include "hardware/i2c.h" #include "pico/stdlib.h" +#include "pico/time.h" #include "logging/log.hh" #include "mcp23017.hh" @@ -21,40 +22,158 @@ static const char * regnames[] = { "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} {} -int -gpios::read(reg r, uint8_t *value) +bool +extender::init() { - uint8_t txdata = uint8_t(r); - int ret = i2c_write_burst_blocking(i2c_default, m_addr, &txdata, 1); - if (ret < 0) { - log::error("writing read address {%02x:%s}: %d", m_addr, regnames[uint8_t(r)], ret); - return ret; + unsigned count = 0; + while (1) { + uint8_t value = 0xba; + int ret = i2c_read_blocking(i2c_default, m_addr, &value, 1, false); + 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 (ret < 0) { - log::error("reading {%02x:%s}: %d", m_addr, regnames[uint8_t(r)], ret); - } else { - log::info("read {%02x:%s}: 0x%02x", m_addr, regnames[uint8_t(r)], *value); + if (read(reg::iodira, m_direction, 2) > 0) { + //log::info("initialized directions to %02x %02x", m_direction[0], m_direction[1]); + return true; } - return ret; + return false; +} + +template +void extender::set_field(unsigned bank, unsigned pin, Value value, uint8_t (¤t)[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(bank, pin, dir, m_direction); +} + +void +extender::set_polarity(unsigned bank, unsigned pin, bool reversed) +{ + set_field(bank, pin, reversed, m_polarity); +} + +void +extender::set_pullup(unsigned bank, unsigned pin, bool enabled) +{ + set_field(bank, pin, enabled, m_pullup); +} + +void +extender::set_irq(unsigned bank, unsigned pin, bool enabled) +{ + set_field(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 -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 }; - int ret = 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; + return i2c_write_blocking(i2c_default, m_addr, txdata, sizeof(txdata), false); } } // namespace mcp23017 diff --git a/src/edmfd/mcp23017.hh b/src/edmfd/mcp23017.hh index 3d11266..f10385c 100644 --- a/src/edmfd/mcp23017.hh +++ b/src/edmfd/mcp23017.hh @@ -16,16 +16,38 @@ enum class reg : uint8_t { olata, olatb, }; -class gpios +enum class direction { out, in }; + +class extender { public: - gpios(uint8_t addr); + extender(uint8_t addr); - int read(reg r, uint8_t *value); - int write(reg r, uint8_t value); + bool init(); + + 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: + template + void set_field(unsigned bank, unsigned pin, Value value, uint8_t (¤t)[2]); + + int write(reg r, uint8_t value); + int read(reg r, uint8_t *value, unsigned count); + 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