diff --git a/firmware/CMakeLists.txt b/firmware/CMakeLists.txt index 984dc25..fd6b7d5 100644 --- a/firmware/CMakeLists.txt +++ b/firmware/CMakeLists.txt @@ -34,5 +34,5 @@ add_otterpill_executable(lambda_nixie_clock ${STM32CubeF0_SOURCES} src/console.cpp src/nixie.cpp src/config.cpp - #src/clock_mode.cpp WIP + src/clock_mode.cpp src/main.cpp) diff --git a/firmware/include/clock_mode.hpp b/firmware/include/clock_mode.hpp new file mode 100644 index 0000000..5c4626b --- /dev/null +++ b/firmware/include/clock_mode.hpp @@ -0,0 +1,72 @@ +/* + * Copyright © 2020 LambdAurora + * + * This file is part of lambda_nixie_clock. + * + * Licensed under the MIT license. For more information, + * see the LICENSE file. + */ + +#ifndef LAMBDA_NIXIE_CLOCK_CLOCK_MODE_HPP +#define LAMBDA_NIXIE_CLOCK_CLOCK_MODE_HPP + +#include +#include +#include +#include + +class ClockManager; + +/** + * Represents a clock mode. + */ +class ClockMode { +public: + virtual void update(ClockManager& manager) = 0; + + /** + * Returns the timeout before the clock manager switches back to the default mode. + * @return 0 to disable the timeout, else a timeout value in seconds. + */ + virtual uint32_t timeout() { + return 0; + } +}; + +class TimeClockMode : public ClockMode { +public: + void update(ClockManager& manager) override; +}; + +class ClockManager { +private: + std::map modes; + std::string _default; + std::string _current_mode; + DS3231 _rtc; + rtc_t _current_clock; + rtc_t _last_clock; + Config _config; + Configuration _config_manager; + +public: + NixieArray nixies; + + explicit ClockManager(const DS3231& rtc, const Configuration& config, std::string default_name, ClockMode* default_mode); + + void register_mode(const std::string& name, ClockMode* mode); + + ClockMode* get_mode(const std::string& name); + + void update(); + + DS3231& rtc(); + + Config& config(); + + Configuration& config_manager(); + + rtc_t& current_clock(); +}; + +#endif //LAMBDA_NIXIE_CLOCK_CLOCK_MODE_HPP diff --git a/firmware/include/common.h b/firmware/include/common.h index c6d9f31..c78e54f 100644 --- a/firmware/include/common.h +++ b/firmware/include/common.h @@ -24,6 +24,10 @@ extern "C" { // The size of the nixie tube array of the clock. // Even numbers are recommended. #define NIXIE_COUNT 8 +// Allow the blinking of the status LED of the OtterPill. +#define STATUS_LED 1 +#define STATUS_LED_GPIO_PORT GPIOB +#define STATUS_LED_GPIO_PIN GPIO_PIN_13 /* Definition for shift registers */ // PA1 -> SDI #define SDI_PIN GPIO_PIN_1 diff --git a/firmware/include/console.hpp b/firmware/include/console.hpp index 4b4586f..8ebd9c4 100644 --- a/firmware/include/console.hpp +++ b/firmware/include/console.hpp @@ -42,6 +42,8 @@ namespace console outstream& operator<<(int i); + outstream& operator<<(size_t i); + outstream& operator<<(char ch); }; diff --git a/firmware/include/nixie.hpp b/firmware/include/nixie.hpp index bdbe937..872f110 100644 --- a/firmware/include/nixie.hpp +++ b/firmware/include/nixie.hpp @@ -139,6 +139,11 @@ class NixieArray return *this; } + /** + * Resets every nixie tube state. + */ + void reset(); + [[nodiscard]] constexpr inline size_t size() const { return NIXIE_COUNT; } diff --git a/firmware/include/rtc.hpp b/firmware/include/rtc.hpp index 063390f..1ad929b 100644 --- a/firmware/include/rtc.hpp +++ b/firmware/include/rtc.hpp @@ -22,6 +22,25 @@ #include #include +/** + * Returns whether the specified year is a leap year. + * @param year The year to check. + * @return True if the year is a leap year, else false. + */ +constexpr bool is_leap_year(uint32_t year); + +/** + * Returns the days in the specified month of the specified year. + * @param month The month. + * @param year The year. + * @return The number of days in month of the specified year. + */ +constexpr uint8_t days_in_month(uint8_t month, uint32_t year); + +constexpr uint16_t days_before_month(uint8_t month, uint32_t year); + +constexpr uint64_t days_before_year(uint32_t year); + /** * Represents the real time clock. */ @@ -33,9 +52,17 @@ typedef struct uint8_t day_of_week; uint8_t day_of_month; uint8_t month; - uint32_t year; + uint32_t year = 0; } rtc_t; +constexpr uint64_t date_to_ordinal(uint8_t day_of_month, uint8_t month, uint32_t year); + +constexpr uint64_t date_to_ordinal(const rtc_t& date); + +constexpr uint8_t get_day_of_week(uint8_t day_of_month, uint8_t month, uint32_t year); + +constexpr uint8_t get_day_of_week(const rtc_t& date); + enum Alarm : uint8_t { ALARM_1 = 0, ALARM_2 = 1 diff --git a/firmware/src/clock_mode.cpp b/firmware/src/clock_mode.cpp new file mode 100644 index 0000000..bd7cd2d --- /dev/null +++ b/firmware/src/clock_mode.cpp @@ -0,0 +1,115 @@ +/* + * Copyright © 2020 LambdAurora + * + * This file is part of lambda_nixie_clock. + * + * Licensed under the MIT license. For more information, + * see the LICENSE file. + */ + +#include +#include + +void TimeClockMode::update(ClockManager& manager) { + bool h24 = manager.config().h24; + + auto hour = manager.current_clock().hour; + // If 12H mode, then fix hour value. + if (!h24 && hour > 12) + hour -= 12; + + auto hour_unit = hour % 10; + hour /= 10; + auto minute = manager.current_clock().minute; + auto minute_unit = minute % 10; + minute /= 10; + auto second = manager.current_clock().second; + auto second_unit = second % 10; + second /= 10; + + auto separator_on = !(second_unit % 2); + + // Reset to avoid display bugs. + manager.nixies.reset(); + + // Straightforward but not very good. +#if NIXIE_COUNT <= 6 + manager.nixies.at(0).number(hour); + manager.nixies.at(1).number(hour_unit); + manager.nixies.at(2).number(minute); + manager.nixies.at(3).number(minute_unit); + if (manager.nixies.size() == 6) { + manager.nixies.at(4).number(second); + manager.nixies.at(5).number(second_unit); + } +#else + // Align to center if there's more than 8 nixie tubes. + size_t start = NIXIE_COUNT / 2 - 4; + manager.nixies.at(start).number(hour); + manager.nixies.at(start + 1).number(hour_unit); + manager.nixies.at(start + 3).number(minute); + manager.nixies.at(start + 4).number(minute_unit); + manager.nixies.at(start + 6).number(second); + manager.nixies.at(start + 7).number(second_unit); + for (size_t i = start + 2; i <= start + 5; i += 3) { + manager.nixies.at(i).left_comma(separator_on); + manager.nixies.at(i).right_comma(separator_on); + } +#endif + + manager.nixies.display(false); +} + +ClockManager::ClockManager(const DS3231& rtc, const Configuration& config, std::string default_name, ClockMode* default_mode) : _default(std::move(default_name)), + _rtc(rtc), _config_manager(config) { + register_mode(this->_default, default_mode); + this->_current_mode = this->_default; + this->_config_manager.read(&this->_config); +} + +void ClockManager::register_mode(const std::string& name, ClockMode* mode) { + this->modes[name] = mode; +} + +ClockMode* ClockManager::get_mode(const std::string& name) { + if (!this->modes.count(name)) + return this->modes[this->_default]; + return this->modes[name]; +} + +void ClockManager::update() { + this->_rtc.get_time(&this->_current_clock); + if (this->_last_clock.year == 0) + this->_last_clock = this->_current_clock; + + // Blink the status LED every seconds if enabled. +#if STATUS_LED == 1 + if (this->_current_clock.second % 2) { + HAL_GPIO_WritePin(STATUS_LED_GPIO_PORT, STATUS_LED_GPIO_PIN, GPIO_PIN_SET); + } else { + HAL_GPIO_WritePin(STATUS_LED_GPIO_PORT, STATUS_LED_GPIO_PIN, GPIO_PIN_RESET); + } +#endif + + ClockMode* mode = this->get_mode(this->_current_mode); + + mode->update(*this); + + this->_last_clock = this->_current_clock; +} + +DS3231& ClockManager::rtc() { + return this->_rtc; +} + +Config& ClockManager::config() { + return this->_config; +} + +Configuration& ClockManager::config_manager() { + return this->_config_manager; +} + +rtc_t& ClockManager::current_clock() { + return this->_current_clock; +} diff --git a/firmware/src/console.cpp b/firmware/src/console.cpp index 96d0325..1b9d877 100644 --- a/firmware/src/console.cpp +++ b/firmware/src/console.cpp @@ -46,6 +46,10 @@ namespace console return this->operator<<(std::to_string(i)); } + outstream& outstream::operator<<(size_t i) { + return this->operator<<(std::to_string(i)); + } + outstream& outstream::operator<<(char ch) { this->_buffer += ch; return *this; diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index f85298a..658e459 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -8,12 +8,7 @@ */ #include -#include -#include -#include - -#define LED_PIN GPIO_PIN_13 -#define INVERT_STATE(state) (state == GPIO_PIN_RESET ? GPIO_PIN_SET : GPIO_PIN_RESET) +#include #define INIT_OUTPUT_GPIO(PIN, PORT) {\ HAL_GPIO_WritePin(PORT, PIN, GPIO_PIN_RESET);\ @@ -36,14 +31,14 @@ static void setup_gpio() { __HAL_RCC_GPIOA_CLK_ENABLE(); // Let's initialize the blinking LED pin. - HAL_GPIO_WritePin(GPIOB, LED_PIN, GPIO_PIN_RESET); + HAL_GPIO_WritePin(STATUS_LED_GPIO_PORT, STATUS_LED_GPIO_PIN, GPIO_PIN_RESET); // HAL GPIO initialization is a bit long honestly. - GPIO_InitStruct.Pin = LED_PIN; + GPIO_InitStruct.Pin = STATUS_LED_GPIO_PIN; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; - HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); + HAL_GPIO_Init(STATUS_LED_GPIO_PORT, &GPIO_InitStruct); // Shift register GPIO. INIT_OUTPUT_GPIO(SDI_PIN, SDI_GPIO_PORT) @@ -77,6 +72,38 @@ int main() { console::setup(); auto ds3231 = DS3231(i2c1h); + if (ds3231.has_lost_power()) { + // If the RTC module lost power, restore the last time and date the firmware knew. + // Mmm dd yyyy + std::string date_str = __DATE__; + // HH:mm:ss + std::string time_str = __TIME__; + + const auto ASCII_ZERO = static_cast('0'); + + rtc_t clock; + clock.hour = (time_str[0] - ASCII_ZERO) * 10 + (time_str[1] - ASCII_ZERO); + clock.minute = (time_str[3] - ASCII_ZERO) * 10 + (time_str[4] - ASCII_ZERO); + clock.second = (time_str[6] - ASCII_ZERO) * 10 + (time_str[7] - ASCII_ZERO); + + std::array months = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; + for (uint8_t i = 0; i < months.size(); i++) { + if (date_str.find(months[i]) != std::string::npos) + clock.month = i + 1; + } + + auto day_of_month_ten = (date_str[4] - ASCII_ZERO) * 10; + if (date_str[4] == ' ') day_of_month_ten = 0; + clock.day_of_month = day_of_month_ten + (date_str[5] - ASCII_ZERO); + clock.year = (date_str[7] - ASCII_ZERO) * 1000 + + (date_str[8] - ASCII_ZERO) * 100 + + (date_str[9] - ASCII_ZERO) * 10 + + (date_str[10] - ASCII_ZERO); + clock.day_of_week = get_day_of_week(clock); + + ds3231.set_time(clock); + } + auto configuration = Configuration({EE24_ADDRESS, i2c1h}); if (!configuration.init()) return 1; @@ -84,11 +111,18 @@ int main() { configuration.reset(); auto nixies = NixieArray(); - nixies.from_string("4;2;0;0;0;0;0;0"); + nixies.reset(); ds3231.enable_oscillator(); - rtc_t clock; + TimeClockMode time_clock_mode; + auto clock = ClockManager(ds3231, configuration, "time", &time_clock_mode); + + while (true) { + clock.update(); + } + + /*rtc_t clock; Config config; ds3231.reset_alarm(ALARM_2); @@ -109,7 +143,7 @@ int main() { nixies.display(false); HAL_Delay(500); - } + }*/ return 0; } diff --git a/firmware/src/nixie.cpp b/firmware/src/nixie.cpp index 2c26efc..36b53e0 100644 --- a/firmware/src/nixie.cpp +++ b/firmware/src/nixie.cpp @@ -9,6 +9,8 @@ #include +const auto ASCII_ZERO = static_cast('0'); + bool NixieState::show_number() const { return this->_show_number; } @@ -66,7 +68,7 @@ void NixieState::from_string(const std::string& str) { for (char c : str) { if (std::isdigit(c)) { show_number = true; - this->number(c - 0x30); // ASCII 0 is 0x30. + this->number(c - ASCII_ZERO); break; } } @@ -78,7 +80,7 @@ void NixieState::from_string(const std::string& str) { std::string NixieState::to_string() const { std::string str = this->_left_comma ? "." : ""; if (this->_show_number) - str.append(static_cast(1), this->_number + 0x30); // ASCII 0 is 0x30. + str.append(static_cast(1), this->_number + ASCII_ZERO); if (this->_right_comma) str.append(","); return str; @@ -130,6 +132,12 @@ NixieState& NixieArray::at(size_t i) { return this->array[i]; } +void NixieArray::reset() { + for (auto& state : this->array) { + state.clear(); + } +} + void NixieArray::display(bool commas) { for (size_t i = NIXIE_COUNT; i > 0; i--) { this->array[i - 1].push(commas); diff --git a/firmware/src/rtc.cpp b/firmware/src/rtc.cpp index 91a4698..e7b71a8 100644 --- a/firmware/src/rtc.cpp +++ b/firmware/src/rtc.cpp @@ -9,6 +9,52 @@ #include +constexpr bool is_leap_year(uint32_t year) { + return year % 4 == 0 && (year % 100 || year % 400 == 0); +} + +constexpr uint8_t days_in_month(uint8_t month, uint32_t year) { + if (month > 12 || month == 0) + return 0; // Who would give an invalid month? + + if ((month <= 7 && month % 2) || (month > 7 && !(month % 2))) + return 31; + else if (month == 2) + return is_leap_year(year) ? 29 : 28; + else + return 30; +} + +constexpr uint16_t days_before_month(uint8_t month, uint32_t year) { + uint16_t result = 0; + for (uint8_t i = 0; i <= month; i++) + result += days_in_month(month, year); + return result; +} + +constexpr uint64_t days_before_year(uint32_t year) { + auto y = year - 1; + return y * 365 + y / 4 - y / 100 + y / 400; +} + +constexpr uint64_t date_to_ordinal(uint8_t day_of_month, uint8_t month, uint32_t year) { + return days_before_year(year) + days_before_month(month, year) + day_of_month; +} + +constexpr uint64_t date_to_ordinal(const rtc_t& date) { + return date_to_ordinal(date.day_of_month, date.month, date.year); +} + +constexpr uint8_t get_day_of_week(uint8_t day_of_month, uint8_t month, uint32_t year) { + // https://cs.uwaterloo.ca/~alopez-o/math-faq/node73.html + year -= month < 3; + return (day_of_month + (year + year / 4 - year / 100 + year / 400) + "-bed=pen+mad."[month]) % 7; +} + +constexpr uint8_t get_day_of_week(const rtc_t& date) { + return get_day_of_week(date.day_of_month, date.month, date.year); +} + /** * Convert normal decimal numbers to binary coded decimal. */