From 75052174a60977d440780f94051ae69d1fba1dd5 Mon Sep 17 00:00:00 2001 From: Mitchell M Date: Sat, 4 Apr 2026 16:35:45 -0500 Subject: [PATCH] game timers --- blk_box.cpp | 2 + drivers/CMakeLists.txt | 2 + drivers/ssegs.cpp | 484 ++++++++++++++++++++++++++++++ drivers/tm1640.cpp | 117 ++++++++ drivers/tm1640.hpp | 47 +++ include/blk_box_drivers/ssegs.hpp | 282 +++++++++++++++++ 6 files changed, 934 insertions(+) create mode 100644 drivers/ssegs.cpp create mode 100644 drivers/tm1640.cpp create mode 100644 drivers/tm1640.hpp create mode 100644 include/blk_box_drivers/ssegs.hpp diff --git a/blk_box.cpp b/blk_box.cpp index 2b081d7..0074916 100644 --- a/blk_box.cpp +++ b/blk_box.cpp @@ -4,10 +4,12 @@ #include "blk_box_drivers/inputs.hpp" #include "blk_box_drivers/leds.hpp" #include "blk_box_drivers/char_lcd.hpp" +#include "blk_box_drivers/ssegs.hpp" void init_blk_box(BlkBoxInitConfig cfg) { init_main_i2c(); init_expander(); init_leds(); init_lcd(); + init_ssegs(); } diff --git a/drivers/CMakeLists.txt b/drivers/CMakeLists.txt index 9cc1c6a..9675c64 100644 --- a/drivers/CMakeLists.txt +++ b/drivers/CMakeLists.txt @@ -6,6 +6,8 @@ set(SOURCES "i2c.cpp" "lcd2004.cpp" "leds.cpp" + "ssegs.cpp" + "tm1640.cpp" ) target_sources(${COMPONENT_LIB} PRIVATE ${SOURCES}) diff --git a/drivers/ssegs.cpp b/drivers/ssegs.cpp new file mode 100644 index 0000000..31c8616 --- /dev/null +++ b/drivers/ssegs.cpp @@ -0,0 +1,484 @@ +#include "blk_box_drivers/ssegs.hpp" + +#include "tm1640.hpp" +#include "pins.h" +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" +#include "freertos/event_groups.h" +#include "esp_log.h" + +#include +#include +#include + +const static uint32_t TICKER_PERIOD_MS = 100; + +const static uint8_t MODULE_IDX = 0; +const static uint8_t GAME_IDX = 4; + +TM1640 ssegs(PIN_SSEG_CLK, PIN_SSEG_DAT); +const static size_t CMD_QUEUE_SIZE = 10; +QueueHandle_t cmd_queue; + +std::atomic game_time = 0; +std::atomic module_time = 0; + +// for notifying users of events +const static uint32_t EVENT_CMDS_FLUSHED = (1 << 0); +const static uint32_t EVENT_MODULE_POSITIVE = (1 << 1); +const static uint32_t EVENT_MODULE_ZERO_NEG = (1 << 2); +const static uint32_t EVENT_GAME_POSITIVE = (1 << 3); +const static uint32_t EVENT_GAME_ZERO_NEG = (1 << 4); + +EventGroupHandle_t ssegs_event_group; + +const static char* TAG = "ssegs"; + +/// Uses a compare-exchange loop to do a saturating subtraction on an atomic int32_t. Returns the old and new values. +std::pair saturating_sub(std::atomic& v, int32_t sub) { + int32_t cur = v.load(std::memory_order_relaxed); + while (true) { + int32_t desired = (cur <= sub) ? 0 : cur - sub; + if (v.compare_exchange_weak(cur, desired, + std::memory_order_relaxed)) { + return {cur, desired}; // {old value, new value} + } + // cur is updated automatically with latest value on failure + } +} + +/// Updates the segment buffer to reflect the current time. +/// +/// Returns `true` if the buffer has changed and needs to be redrawn. +/// +/// `seg_buf.len()` should be >= 4. +bool update_segments(int32_t last_time, int32_t current_time, uint8_t seg_buf[4]) { + const uint32_t MILLIS_10S = 100; + const uint32_t SECOND = 1000; + const uint32_t SECOND_10S = SECOND * 10; + const uint32_t MINUTE = 60 * SECOND; + const uint32_t MINUTE_10S = 10 * MINUTE; + const uint32_t HOUR = 60 * MINUTE; + const uint32_t HOUR_10S = 10 * HOUR; + + uint32_t time = std::abs(current_time); + + if (time > HOUR) { + // HH.MM + if ((current_time / MINUTE) == (last_time / MINUTE)) { + // no change neccesary + return false; + } + + uint8_t h1 = (time / HOUR_10S) % 10; + uint8_t h0 = (time / HOUR) % 10; + uint8_t minutes = (time / MINUTE) % 60; + uint8_t m1 = minutes / 10; + uint8_t m0 = minutes % 10; + + seg_buf[0] = SSegController::FONT_HEX[h1]; + seg_buf[1] = SSegController::FONT_HEX[h0] | SSegController::BIT_MASK_DP; + seg_buf[2] = SSegController::FONT_HEX[m1]; + seg_buf[3] = SSegController::FONT_HEX[m0]; + return true; + } else if (time > MINUTE) { + // MM.SS + if ((current_time / SECOND) == (last_time / SECOND)) { + // no change neccesary + return false; + } + + uint8_t m1 = (time / MINUTE_10S) % 10; + uint8_t m0 = (time / MINUTE) % 10; + uint8_t seconds = (time / SECOND) % 60; + uint8_t s1 = seconds / 10; + uint8_t s0 = seconds % 10; + + seg_buf[0] = SSegController::FONT_HEX[m1]; + seg_buf[1] = SSegController::FONT_HEX[m0] | SSegController::BIT_MASK_DP; + seg_buf[2] = SSegController::FONT_HEX[s1]; + seg_buf[3] = SSegController::FONT_HEX[s0]; + return true; + } else { + // SS.m + if ((current_time / MILLIS_10S) == (last_time / MILLIS_10S)) { + // no change neccesary + return false; + } + + uint8_t s1 = (time / SECOND_10S) % 10; + uint8_t s0 = (time / SECOND) % 10; + uint8_t m1 = (time / MILLIS_10S) % 10; + + seg_buf[0] = 0; // unused digit + seg_buf[1] = SSegController::FONT_HEX[s1]; + seg_buf[2] = SSegController::FONT_HEX[s0] | SSegController::BIT_MASK_DP; + seg_buf[3] = SSegController::FONT_HEX[m1]; + return true; + } + + return true; +} + +static void timer_task(void* arg) { + (void) arg; + + const TickType_t ticker_period_ticks = pdMS_TO_TICKS(TICKER_PERIOD_MS); + + ESP_LOGI(TAG, "sseg timer task starting..."); + + bool game_en = false; + bool game_running = false; + bool game_rollover = true; + bool module_en = false; + bool module_running = false; + bool module_rollover = false; + + uint8_t seg_buf[4] = {0}; + + TickType_t last_wake_time = xTaskGetTickCount(); + SSegCommand cmd; + + while (true) { + TickType_t elapsed = xTaskGetTickCount() - last_wake_time; + if ((ticker_period_ticks > elapsed) && (xQueueReceive(cmd_queue, &cmd, ticker_period_ticks - elapsed) == pdPASS)) { + // command received + ESP_LOGI(TAG, "sseg command received"); + + switch (cmd.type) { + case SSegCommand::Type::SetIntensity: { + uint8_t intensity = std::get(cmd.data); + ssegs.set_intensity(intensity); + break; + } + case SSegCommand::Type::EnableGameTimer: { + game_en = true; + int32_t game_time_val = game_time.load(std::memory_order_acquire); + if (update_segments(std::numeric_limits::max(), game_time_val, seg_buf)) { + ssegs.set_digits(GAME_IDX, seg_buf, 4); + } + break; + } + case SSegCommand::Type::DisableGameTimer: { + game_en = false; + game_running = false; + game_time.store(0, std::memory_order_release); + xEventGroupClearBits(ssegs_event_group, EVENT_GAME_POSITIVE | EVENT_GAME_ZERO_NEG); + for (uint8_t& seg : seg_buf) { + seg = 0; + } + ssegs.set_digits(GAME_IDX, seg_buf, 4); + break; + } + case SSegCommand::Type::StartGameTimer: + game_running = true; + break; + case SSegCommand::Type::StopGameTimer: + game_running = false; + break; + case SSegCommand::Type::SetGameTime: { + int32_t new_time = std::get(cmd.data); + int32_t last_time = game_time.exchange(new_time, std::memory_order_acq_rel); + if (new_time > 0) { + xEventGroupSetBits(ssegs_event_group, EVENT_GAME_POSITIVE); + xEventGroupClearBits(ssegs_event_group, EVENT_GAME_ZERO_NEG); + } else { + xEventGroupSetBits(ssegs_event_group, EVENT_GAME_ZERO_NEG); + xEventGroupClearBits(ssegs_event_group, EVENT_GAME_POSITIVE); + } + if (game_en) { + if (update_segments(last_time, new_time, seg_buf)) { + ssegs.set_digits(GAME_IDX, seg_buf, 4); + } + } + break; + } + case SSegCommand::Type::EnableModuleTimer: { + module_en = true; + int32_t module_time_val = module_time.load(std::memory_order_acquire); + if (update_segments(std::numeric_limits::max(), module_time_val, seg_buf)) { + ssegs.set_digits(MODULE_IDX, seg_buf, 4); + } + break; + } + case SSegCommand::Type::DisableModuleTimer: { + module_en = false; + module_running = false; + module_time.store(0, std::memory_order_release); + xEventGroupClearBits(ssegs_event_group, EVENT_MODULE_POSITIVE | EVENT_MODULE_ZERO_NEG); + for (uint8_t& seg : seg_buf) { + seg = 0; + } + ssegs.set_digits(MODULE_IDX, seg_buf, 4); + break; + } + case SSegCommand::Type::StartModuleTimer: + module_running = true; + break; + case SSegCommand::Type::StopModuleTimer: + module_running = false; + break; + case SSegCommand::Type::SetModuleTime: { + int32_t new_time = std::get(cmd.data); + int32_t last_time = module_time.exchange(new_time, std::memory_order_acq_rel); + if (new_time > 0) { + xEventGroupSetBits(ssegs_event_group, EVENT_MODULE_POSITIVE); + xEventGroupClearBits(ssegs_event_group, EVENT_MODULE_ZERO_NEG); + } else { + xEventGroupSetBits(ssegs_event_group, EVENT_MODULE_ZERO_NEG); + xEventGroupClearBits(ssegs_event_group, EVENT_MODULE_POSITIVE); + } + if (module_en) { + if (update_segments(last_time, new_time, seg_buf)) { + ssegs.set_digits(MODULE_IDX, seg_buf, 4); + } + } + break; + } + case SSegCommand::Type::SetGameRaw: { + std::array raw = std::get>(cmd.data); + ssegs.set_digits(GAME_IDX, raw.data(), 4); + break; + } + case SSegCommand::Type::SetGameDigit: { + auto [digit, value] = std::get>(cmd.data); + ssegs.set_digit(GAME_IDX + digit, value); + break; + } + case SSegCommand::Type::SetModuleRaw: { + std::array raw = std::get>(cmd.data); + ssegs.set_digits(MODULE_IDX, raw.data(), 4); + break; + } + case SSegCommand::Type::SetModuleDigit: { + auto [digit, value] = std::get>(cmd.data); + ssegs.set_digit(MODULE_IDX + digit, value); + break; + } + + case SSegCommand::Type::SetGameRollover: { + bool rollover = std::get(cmd.data); + game_rollover = rollover; + break; + } + case SSegCommand::Type::SetModuleRollover: { + bool rollover = std::get(cmd.data); + module_rollover = rollover; + break; + } + } + + if (uxQueueMessagesWaiting(cmd_queue) == 0) { + xEventGroupSetBits(ssegs_event_group, EVENT_CMDS_FLUSHED); + } + } else { + // ticker finished + last_wake_time += pdMS_TO_TICKS(TICKER_PERIOD_MS); + + bool update_module = module_en && module_running; + bool update_game = game_en && game_running; + + // ESP_LOGI(TAG, "ticker ticked: update_game=%d, update_module=%d", update_game, update_module); + + if (update_module) { + int32_t old_time; + int32_t new_time; + if (module_rollover) { + old_time = module_time.fetch_sub(TICKER_PERIOD_MS); + new_time = old_time - TICKER_PERIOD_MS; // fetch_sub returns old value + } else { + std::tie(old_time, new_time) = saturating_sub(module_time, TICKER_PERIOD_MS); + } + + if (new_time > 0) { + xEventGroupSetBits(ssegs_event_group, EVENT_MODULE_POSITIVE); + xEventGroupClearBits(ssegs_event_group, EVENT_MODULE_ZERO_NEG); + } else { + xEventGroupSetBits(ssegs_event_group, EVENT_MODULE_ZERO_NEG); + xEventGroupClearBits(ssegs_event_group, EVENT_MODULE_POSITIVE); + } + + if (update_segments(old_time, new_time, seg_buf)) { + ssegs.set_digits(MODULE_IDX, seg_buf, 4); + } + if (new_time == 0 && !module_rollover) { + // we've hit 0 and are not rolling over + module_running = false; + } + } + if (update_game) { + int32_t old_time; + int32_t new_time; + if (game_rollover) { + old_time = game_time.fetch_sub(TICKER_PERIOD_MS); + new_time = old_time - TICKER_PERIOD_MS; // fetch_sub returns old value + } else { + std::tie(old_time, new_time) = saturating_sub(game_time, TICKER_PERIOD_MS); + } + + if (new_time > 0) { + xEventGroupSetBits(ssegs_event_group, EVENT_GAME_POSITIVE); + xEventGroupClearBits(ssegs_event_group, EVENT_GAME_ZERO_NEG); + } else { + xEventGroupSetBits(ssegs_event_group, EVENT_GAME_ZERO_NEG); + xEventGroupClearBits(ssegs_event_group, EVENT_GAME_POSITIVE); + } + + if (update_segments(old_time, new_time, seg_buf)) { + ssegs.set_digits(GAME_IDX, seg_buf, 4); + } + if (new_time == 0 && !game_rollover) { + // we've hit 0 and are not rolling over + game_running = false; + } + } + } + } +} + +// SSegController static method implementations +void SSegController::enable_game_timer() { + xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED); + SSegCommand cmd = SSegCommand::EnableGameTimer(); + xQueueSend(cmd_queue, &cmd, portMAX_DELAY); +} + +void SSegController::disable_game_timer() { + xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED); + SSegCommand cmd = SSegCommand::DisableGameTimer(); + xQueueSend(cmd_queue, &cmd, portMAX_DELAY); +} + +void SSegController::start_game_timer() { + xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED); + SSegCommand cmd = SSegCommand::StartGameTimer(); + xQueueSend(cmd_queue, &cmd, portMAX_DELAY); +} + +void SSegController::stop_game_timer() { + xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED); + SSegCommand cmd = SSegCommand::StopGameTimer(); + xQueueSend(cmd_queue, &cmd, portMAX_DELAY); +} + +void SSegController::set_game_time(int32_t millis) { + // Align to TICKER_PERIOD_MS + millis = millis - (millis % TICKER_PERIOD_MS); + xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED); + SSegCommand cmd = SSegCommand::SetGameTime(millis); + xQueueSend(cmd_queue, &cmd, portMAX_DELAY); +} + +void SSegController::enable_module_timer() { + xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED); + SSegCommand cmd = SSegCommand::EnableModuleTimer(); + xQueueSend(cmd_queue, &cmd, portMAX_DELAY); +} + +void SSegController::disable_module_timer() { + xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED); + SSegCommand cmd = SSegCommand::DisableModuleTimer(); + xQueueSend(cmd_queue, &cmd, portMAX_DELAY); +} + +void SSegController::start_module_timer() { + xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED); + SSegCommand cmd = SSegCommand::StartModuleTimer(); + xQueueSend(cmd_queue, &cmd, portMAX_DELAY); +} + +void SSegController::stop_module_timer() { + xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED); + SSegCommand cmd = SSegCommand::StopModuleTimer(); + xQueueSend(cmd_queue, &cmd, portMAX_DELAY); +} + +void SSegController::set_module_time(int32_t millis) { + // Align to TICKER_PERIOD_MS + millis = millis - (millis % TICKER_PERIOD_MS); + xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED); + SSegCommand cmd = SSegCommand::SetModuleTime(millis); + xQueueSend(cmd_queue, &cmd, portMAX_DELAY); +} + +void SSegController::set_game_raw(const std::array& segments) { + xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED); + SSegCommand cmd = SSegCommand::SetGameRaw(segments); + xQueueSend(cmd_queue, &cmd, portMAX_DELAY); +} + +void SSegController::set_game_digit_raw(uint8_t digit, uint8_t segments) { + xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED); + SSegCommand cmd = SSegCommand::SetGameDigit(digit, segments); + xQueueSend(cmd_queue, &cmd, portMAX_DELAY); +} + +void SSegController::set_module_raw(const std::array& segments) { + xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED); + SSegCommand cmd = SSegCommand::SetModuleRaw(segments); + xQueueSend(cmd_queue, &cmd, portMAX_DELAY); +} + +void SSegController::set_module_digit_raw(uint8_t digit, uint8_t segments) { + xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED); + SSegCommand cmd = SSegCommand::SetModuleDigit(digit, segments); + xQueueSend(cmd_queue, &cmd, portMAX_DELAY); +} + +void SSegController::game_timer_rollover(bool rollover) { + xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED); + SSegCommand cmd = SSegCommand::SetGameRollover(rollover); + xQueueSend(cmd_queue, &cmd, portMAX_DELAY); +} + +void SSegController::module_timer_rollover(bool rollover) { + xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED); + SSegCommand cmd = SSegCommand::SetModuleRollover(rollover); + xQueueSend(cmd_queue, &cmd, portMAX_DELAY); +} + +int32_t SSegController::get_game_time() { + return game_time.load(std::memory_order_acquire); +} + +int32_t SSegController::get_module_time() { + return module_time.load(std::memory_order_acquire); +} + +void SSegController::flush() { + xEventGroupWaitBits(ssegs_event_group, EVENT_CMDS_FLUSHED, pdFALSE, pdTRUE, portMAX_DELAY); +} + +void SSegController::wait_game_timer_done() { + xEventGroupWaitBits(ssegs_event_group, EVENT_GAME_ZERO_NEG, pdTRUE, pdFALSE, portMAX_DELAY); +} + +void SSegController::wait_module_timer_done() { + xEventGroupWaitBits(ssegs_event_group, EVENT_MODULE_ZERO_NEG, pdTRUE, pdFALSE, portMAX_DELAY); +} + +void SSegController::set_intensity(uint8_t intensity) { + xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED); + SSegCommand cmd = SSegCommand::SetIntensity(intensity); + xQueueSend(cmd_queue, &cmd, portMAX_DELAY); +} + +void init_ssegs() { + ssegs.init(); + + cmd_queue = xQueueCreate(CMD_QUEUE_SIZE, sizeof(SSegCommand)); + if (cmd_queue == NULL) { + ESP_LOGE(TAG, "Failed to create command queue!"); + return; + } + + ssegs_event_group = xEventGroupCreate(); + if (ssegs_event_group == NULL) { + ESP_LOGE(TAG, "Failed to create event group!"); + return; + } + + xTaskCreate(timer_task, "ssegs_timer_task", 4096, NULL, 4, NULL); +} + + diff --git a/drivers/tm1640.cpp b/drivers/tm1640.cpp new file mode 100644 index 0000000..eb688e8 --- /dev/null +++ b/drivers/tm1640.cpp @@ -0,0 +1,117 @@ +#include "tm1640.hpp" +#include +#include "driver/gpio.h" +#include "esp_rom_sys.h" + +// Constants +static const uint8_t CMD_DATA_AUTO = 0x40; +static const uint8_t CMD_DATA_FIXED = 0x44; +static const uint8_t CMD_DISPLAY = 0x80; +static const uint8_t CMD_ADDRESS = 0xC0; + +// TODO: we could use the RMT interface to do this more efficiently. + +TM1640::TM1640(gpio_num_t clk_pin, gpio_num_t dio_pin) : clk_pin(clk_pin), dio_pin(dio_pin), intensity(0x0F) { + // Configure pins as output + gpio_config_t io_conf = {}; + io_conf.intr_type = GPIO_INTR_DISABLE; + io_conf.mode = GPIO_MODE_OUTPUT; + io_conf.pin_bit_mask = (1ULL << clk_pin) | (1ULL << dio_pin); + io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + io_conf.pull_up_en = GPIO_PULLUP_DISABLE; + gpio_config(&io_conf); + + // Set pins high + gpio_set_level(clk_pin, 1); + gpio_set_level(dio_pin, 1); +} + +void TM1640::bit_delay() { + esp_rom_delay_us(1); +} + +void TM1640::start() { + gpio_set_level(dio_pin, 0); + gpio_set_level(clk_pin, 0); + bit_delay(); +} + +void TM1640::stop() { + gpio_set_level(dio_pin, 0); + bit_delay(); + gpio_set_level(clk_pin, 1); + gpio_set_level(dio_pin, 1); + bit_delay(); +} + +void TM1640::shift_out(uint8_t data) { + for (int i = 0; i < 8; i++) { + gpio_set_level(dio_pin, data & 1); + data >>= 1; + bit_delay(); + gpio_set_level(clk_pin, 1); + bit_delay(); + gpio_set_level(clk_pin, 0); + bit_delay(); + } +} + +void TM1640::send(uint8_t* data, size_t len) { + start(); + for (size_t i = 0; i < len; i++) { + shift_out(data[i]); + } + stop(); +} + +void TM1640::init() { + clear_display(); +} + +void TM1640::clear_display() { + uint8_t data1[] = {CMD_DATA_AUTO}; + send(data1, 1); + uint8_t data2[] = {CMD_ADDRESS, 0, 0, 0, 0, 0, 0, 0, 0}; + send(data2, 9); + uint8_t data3 = CMD_DISPLAY | intensity; + send(&data3, 1); +} + +// TODO: can these become all one send?? +// other functions too +void TM1640::set_digit(uint8_t digit, uint8_t segments) { + uint8_t data1[] = {CMD_DATA_FIXED}; + send(data1, 1); + uint8_t cmd = CMD_ADDRESS | digit; + uint8_t data2[] = {cmd, segments}; + send(data2, 2); + uint8_t data3 = CMD_DISPLAY | intensity; + send(&data3, 1); +} + +void TM1640::set_digits(uint8_t starting_pos, uint8_t* segments, size_t len) { + uint8_t data1[] = {CMD_DATA_AUTO}; + send(data1, 1); + std::vector data; + data.push_back(CMD_ADDRESS | starting_pos); + for (size_t i = 0; i < len; i++) { + data.push_back(segments[i]); + } + send(data.data(), data.size()); + uint8_t data3 = CMD_DISPLAY | intensity; + send(&data3, 1); +} + +void TM1640::set_intensity(uint8_t intensity) { + uint8_t new_intensity = intensity & 0x07; // 0-7 + this->intensity = (this->intensity & 0xF8) | new_intensity; + uint8_t cmd = CMD_DISPLAY | this->intensity; + send(&cmd, 1); +} + +void TM1640::set_display(bool on) { + uint8_t display_bit = on ? 0x08 : 0x00; + this->intensity = (this->intensity & 0xF7) | display_bit; + uint8_t cmd = CMD_DISPLAY | this->intensity; + send(&cmd, 1); +} diff --git a/drivers/tm1640.hpp b/drivers/tm1640.hpp new file mode 100644 index 0000000..d7835ae --- /dev/null +++ b/drivers/tm1640.hpp @@ -0,0 +1,47 @@ +#ifndef TM1640_HPP +#define TM1640_HPP + +#include "driver/gpio.h" +#include + +class TM1640 { + gpio_num_t clk_pin; + gpio_num_t dio_pin; + /// The intensity and display on/off setting. + uint8_t intensity; + + void bit_delay(); + void start(); + void stop(); + void shift_out(uint8_t data); + void send(uint8_t* data, size_t len); +public: + TM1640(gpio_num_t clk_pin, gpio_num_t dio_pin); + + /// Initializes the TM1640 7-segment display. + void init(); + + /// Clears the display by setting all segments to off. + void clear_display(); + + /// Sets the segments of a single digit. + void set_digit(uint8_t digit, uint8_t segments); + + /// Sets the segments of multiple digits starting at `starting_pos`. + void set_digits(uint8_t starting_pos, uint8_t* segments, size_t len); + + /// Sets the intensity from 0-7. + /// + /// intensity 0 is still on. To turn the display off, use `set_display`(). + void set_intensity(uint8_t intensity); + + /// Turns the display on or off. + void set_display(bool on); + + +}; + + + + +#endif // TM1640_HPP diff --git a/include/blk_box_drivers/ssegs.hpp b/include/blk_box_drivers/ssegs.hpp new file mode 100644 index 0000000..1b0f2cd --- /dev/null +++ b/include/blk_box_drivers/ssegs.hpp @@ -0,0 +1,282 @@ +#ifndef SSEGS_HPP +#define SSEGS_HPP + +#include +#include +#include +#include + +/// A command to send to the sseg timer controller. +struct SSegCommand { + enum class Type { + SetIntensity, + EnableGameTimer, + DisableGameTimer, + StartGameTimer, + StopGameTimer, + SetGameTime, + EnableModuleTimer, + DisableModuleTimer, + StartModuleTimer, + StopModuleTimer, + SetModuleTime, + SetGameRaw, + SetGameDigit, + SetModuleRaw, + SetModuleDigit, + SetGameRollover, + SetModuleRollover, + }; + + Type type; + std::variant< + std::monostate, // for commands without data + uint8_t, // SetIntensity + int32_t, // SetGameTime, SetModuleTime + std::array, // SetGameRaw, SetModuleRaw + std::pair, // SetGameDigit, SetModuleDigit + bool // SetGameRollover, SetModuleRollover + > data; + + // Constructors for each variant + static SSegCommand SetIntensity(uint8_t intensity) { + return {Type::SetIntensity, intensity}; + } + + static SSegCommand EnableGameTimer() { + return {Type::EnableGameTimer, std::monostate{}}; + } + + static SSegCommand DisableGameTimer() { + return {Type::DisableGameTimer, std::monostate{}}; + } + + static SSegCommand StartGameTimer() { + return {Type::StartGameTimer, std::monostate{}}; + } + + static SSegCommand StopGameTimer() { + return {Type::StopGameTimer, std::monostate{}}; + } + + static SSegCommand SetGameTime(int32_t time) { + return {Type::SetGameTime, time}; + } + + static SSegCommand EnableModuleTimer() { + return {Type::EnableModuleTimer, std::monostate{}}; + } + + static SSegCommand DisableModuleTimer() { + return {Type::DisableModuleTimer, std::monostate{}}; + } + + static SSegCommand StartModuleTimer() { + return {Type::StartModuleTimer, std::monostate{}}; + } + + static SSegCommand StopModuleTimer() { + return {Type::StopModuleTimer, std::monostate{}}; + } + + static SSegCommand SetModuleTime(int32_t time) { + return {Type::SetModuleTime, time}; + } + + static SSegCommand SetGameRaw(std::array raw) { + return {Type::SetGameRaw, raw}; + } + + static SSegCommand SetGameDigit(uint8_t digit, uint8_t value) { + return {Type::SetGameDigit, std::make_pair(digit, value)}; + } + + static SSegCommand SetModuleRaw(std::array raw) { + return {Type::SetModuleRaw, raw}; + } + + static SSegCommand SetModuleDigit(uint8_t digit, uint8_t value) { + return {Type::SetModuleDigit, std::make_pair(digit, value)}; + } + + static SSegCommand SetGameRollover(bool rollover) { + return {Type::SetGameRollover, rollover}; + } + + static SSegCommand SetModuleRollover(bool rollover) { + return {Type::SetModuleRollover, rollover}; + } +}; + +class SSegController { +public: + /// A hexidecimal font for the seven segment displays. + constexpr static uint8_t FONT_HEX[16] = { + 0b00111111, 0b00000110, 0b01011011, 0b01001111, 0b01100110, 0b01101101, 0b01111101, + 0b00000111, 0b01111111, 0b01101111, 0b01110111, 0b01111100, 0b00111001, 0b01011110, + 0b01111001, 0b01110001, + }; + /// The mask for the 'A' segment of the display. + constexpr static uint8_t BIT_MASK_A = 0b0000'0001; + /// The mask for the 'B' segment of the display. + constexpr static uint8_t BIT_MASK_B = 0b0000'0010; + /// The mask for the 'C' segment of the display. + constexpr static uint8_t BIT_MASK_C = 0b0000'0100; + /// The mask for the 'D' segment of the display. + constexpr static uint8_t BIT_MASK_D = 0b0000'1000; + /// The mask for the 'E' segment of the display. + constexpr static uint8_t BIT_MASK_E = 0b0001'0000; + /// The mask for the 'F' segment of the display. + constexpr static uint8_t BIT_MASK_F = 0b0010'0000; + /// The mask for the 'G' segment of the display. + constexpr static uint8_t BIT_MASK_G = 0b0100'0000; + /// The mask for the 'DP' (decimal point) segment of the display. + constexpr static uint8_t BIT_MASK_DP = 0b1000'0000; + + /// Enables the game timer. + /// + /// This "gives control" of the game timer over to the + /// timer task. + /// + /// This does not start the game timer, only enables it. + static void enable_game_timer(); + + /// Disables the game timer. + /// + /// This "takes control" of the game timer away from the + /// timer task. + /// + /// This also stops the timer, resets the time to 0, and clears + /// the display. + static void disable_game_timer(); + + /// Starts the game timer. + /// + /// This can be called while the game timer is disabled, + /// but the timer will not start until it is enabled. + /// + /// Calling this while the timer is disabled can be useful + /// if you want it to start counting right away. + static void start_game_timer(); + + /// Stops the game timer. + /// + /// This can be called while the game timer is disabled, + /// but the timer only counts while it is enabled regardless. + static void stop_game_timer(); + + /// Sets the game time. + /// + /// This can be called even when the game timer is disabled. + /// + /// A negative number will cause the timer to count up. + static void set_game_time(int32_t millis); + + /// Enables the module timer. + /// + /// This "gives control" of the module timer over to the + /// timer task. + /// + /// This does not start the module timer, only enables it. + static void enable_module_timer(); + + /// Disables the module timer. + /// + /// This "takes control" of the module timer away from the + /// timer task. + /// + /// This also stops the timer, resets the time to 0, and clears + /// the display. + static void disable_module_timer(); + + /// Starts the module timer. + /// + /// This can be called while the module timer is disabled, + /// but the timer will not start until it is enabled. + /// + /// Calling this while the timer is disabled can be useful + /// if you want it to start counting right away. + static void start_module_timer(); + + /// Stops the module timer. + /// + /// This can be called while the module timer is disabled, + /// but the timer only counts while it is enabled regardless. + static void stop_module_timer(); + + /// Sets the module time. + /// + /// This can be called even when the module timer is disabled. + /// + /// A negative number will cause the timer to count up. + static void set_module_time(int32_t millis); + + /// Sets the game timer to the given raw segments. + /// + /// You should ensure the game timer is disabled before + /// calling this, otherwise, the data will be overwritten. + static void set_game_raw(const std::array& segments); + + /// Sets the game timer digit to the given raw segments. + /// + /// You should ensure the game timer is disabled before + /// calling this, otherwise, the data will be overwritten. + /// + /// `digit` should be in the range 0..=3. + static void set_game_digit_raw(uint8_t digit, uint8_t segments); + + /// Sets the module timer to the given raw segments. + /// + /// You should ensure the module timer is disabled before + /// calling this, otherwise, the data will be overwritten. + static void set_module_raw(const std::array& segments); + + /// Sets the module timer digit to the given raw segments. + /// + /// You should ensure the module timer is disabled before + /// calling this, otherwise, the data will be overwritten. + /// + /// `digit` should be in the range 0..=3. + static void set_module_digit_raw(uint8_t digit, uint8_t segments); + + /// Sets the rollover logic for the game timer. + /// + /// If `true`, when the timer reaches zero, it will go + /// negative, and start counting up. + /// If `false`, when the timer reaches zero, it will stop + /// the timer. + static void game_timer_rollover(bool rollover); + + /// Sets the rollover logic for the module timer. + /// + /// If `true`, when the timer reaches zero, it will go + /// negative, and start counting up. + /// If `false`, when the timer reaches zero, it will stop + /// the timer. + static void module_timer_rollover(bool rollover); + + /// Gets the current game time in millis. + static int32_t get_game_time(); + + /// Gets the current module time in millis. + static int32_t get_module_time(); + + /// Waits until all commands are flushed to the seven segments. + static void flush(); + + /// Waits until the game timer is zero (or negative). + static void wait_game_timer_done(); + + /// Waits until the module timer is zero (or negative). + static void wait_module_timer_done(); + + /// Sets the intensity of the display. + /// + /// `intensity` gets clamped to the range `0..=7` + static void set_intensity(uint8_t intensity); + +}; + +void init_ssegs(); + +#endif // SSEGS_HPP