Skip to content

Commit 871e562

Browse files
committed
Implement getting time from RTC if NTP is not available
- Implement an I2C OOP wrapper around ESP-IDF's i2c_master - Implement get/set time for DS3231 RTC - Integrate RTC handling to NixieClock
1 parent 5800510 commit 871e562

7 files changed

Lines changed: 341 additions & 4 deletions

File tree

firmware/main/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ idf_component_register(
1010
SRCS
1111
bcd_2_decimal_decoder.cpp
1212
config_store.cpp
13+
ds3231.cpp
14+
i2c_bus.cpp
1315
in14_nixie_tube.cpp
1416
led_controller.cpp
1517
led_info.cpp

firmware/main/ds3231.cpp

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/******************************************************************************
2+
* File: ds3231.h
3+
* Author: Daniel Knezevic
4+
* Year: 2025
5+
* Brief: Definitions for the DS3231 real-time clock driver.
6+
******************************************************************************/
7+
8+
#include "ds3231.h"
9+
10+
static constexpr uint8_t kAddr = 0x68;
11+
static constexpr u_int32_t kFreq = 400000; // Hz
12+
static constexpr uint8_t kTimeReg = 0x00;
13+
14+
Ds3231::Ds3231(I2cBus& bus) : mBus(bus) {}
15+
16+
void Ds3231::initialize() {
17+
ESP_ERROR_CHECK(mBus.addDevice(kAddr, kFreq, &mDevHandle));
18+
}
19+
20+
bool Ds3231::getTime(struct tm* tm) {
21+
uint8_t reg = kTimeReg;
22+
uint8_t buf[7];
23+
if (mBus.read(mDevHandle, &reg, 1, buf, sizeof(buf)) != ESP_OK) {
24+
return false;
25+
}
26+
tm->tm_sec = bcd2dec(buf[0]);
27+
tm->tm_min = bcd2dec(buf[1]);
28+
tm->tm_hour = bcd2dec(buf[2]);
29+
tm->tm_mday = bcd2dec(buf[4]);
30+
tm->tm_mon = bcd2dec(buf[5]) - 1;
31+
tm->tm_year = bcd2dec(buf[6]) + 100;
32+
return true;
33+
}
34+
35+
bool Ds3231::setTime(const struct tm* tm) {
36+
uint8_t buf[8];
37+
buf[0] = kTimeReg; // register address
38+
buf[1] = dec2bcd(tm->tm_sec);
39+
buf[2] = dec2bcd(tm->tm_min);
40+
buf[3] = dec2bcd(tm->tm_hour);
41+
buf[4] = 0; // weekday (not used)
42+
buf[5] = dec2bcd(tm->tm_mday);
43+
buf[6] = dec2bcd(tm->tm_mon + 1);
44+
buf[7] = dec2bcd(tm->tm_year - 100);
45+
if (mBus.write(mDevHandle, buf, sizeof(buf)) != ESP_OK) {
46+
return false;
47+
}
48+
return true;
49+
}
50+
51+
uint8_t Ds3231::bcd2dec(uint8_t val) { return (val >> 4) * 10 + (val & 0x0F); }
52+
53+
uint8_t Ds3231::dec2bcd(uint8_t val) { return ((val / 10) << 4) | (val % 10); }

firmware/main/i2c_bus.cpp

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/******************************************************************************
2+
* File: i2c_bus.cpp
3+
* Author: Daniel Knezevic
4+
* Year: 2025
5+
* Brief: Object-oriented wrapper around the ESP-IDF I2C master driver
6+
******************************************************************************/
7+
8+
#include "i2c_bus.h"
9+
10+
#include "freertos/FreeRTOS.h"
11+
#include "freertos/task.h"
12+
13+
I2cBus::I2cBus(i2c_port_t port, gpio_num_t sda, gpio_num_t scl)
14+
: mPort(port), mSda(sda), mScl(scl), mBus(nullptr) {}
15+
16+
I2cBus::~I2cBus() {
17+
if (mBus) {
18+
i2c_del_master_bus(mBus);
19+
mBus = nullptr;
20+
}
21+
}
22+
23+
esp_err_t I2cBus::initialize() {
24+
i2c_master_bus_config_t config = {};
25+
config.i2c_port = mPort;
26+
config.sda_io_num = mSda;
27+
config.scl_io_num = mScl;
28+
config.clk_source = I2C_CLK_SRC_DEFAULT;
29+
config.glitch_ignore_cnt = 7;
30+
config.flags = {.enable_internal_pullup = true};
31+
return i2c_new_master_bus(&config, &mBus);
32+
}
33+
34+
esp_err_t I2cBus::addDevice(uint8_t address, uint32_t freq,
35+
i2c_master_dev_handle_t* dev) {
36+
i2c_device_config_t dev_cfg = {.device_address = address,
37+
.scl_speed_hz = freq};
38+
return i2c_master_bus_add_device(mBus, &dev_cfg, dev);
39+
}
40+
41+
esp_err_t I2cBus::write(i2c_master_dev_handle_t dev, const uint8_t* data,
42+
size_t len, uint32_t timeout_ms) {
43+
return i2c_master_transmit(dev, data, len, pdMS_TO_TICKS(timeout_ms));
44+
}
45+
46+
esp_err_t I2cBus::read(i2c_master_dev_handle_t dev, const uint8_t* reg,
47+
size_t reg_len, uint8_t* data, size_t data_len,
48+
uint32_t timeout_ms) {
49+
// If reg_len == 0, just read
50+
if (reg_len > 0) {
51+
esp_err_t ret =
52+
i2c_master_transmit(dev, reg, reg_len, pdMS_TO_TICKS(timeout_ms));
53+
if (ret != ESP_OK)
54+
return ret;
55+
}
56+
return i2c_master_receive(dev, data, data_len, pdMS_TO_TICKS(timeout_ms));
57+
}

firmware/main/include/ds3231.h

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/******************************************************************************
2+
* File: ds3231.h
3+
* Author: Daniel Knezevic
4+
* Year: 2025
5+
* Brief: Declarations for the DS3231 real-time clock driver.
6+
******************************************************************************/
7+
8+
#ifndef ds3231_h
9+
#define ds3231_h
10+
11+
#include "i2c_bus.h"
12+
13+
#include <ctime>
14+
15+
class Ds3231 {
16+
public:
17+
/**
18+
* @brief Construct the DS3231 driver using a shared I2C bus.
19+
* @param i2c Reference to an initialized I2C object.
20+
*/
21+
Ds3231(I2cBus& bus);
22+
23+
/**
24+
* @brief Initialize the module
25+
*/
26+
void initialize();
27+
28+
/**
29+
* @brief Read current time and date from the RTC.
30+
* @param[out] timeinfo Pointer to a struct tm to receive the data.
31+
* @return true on success, false otherwise.
32+
*/
33+
bool getTime(struct tm* timeinfo);
34+
35+
/**
36+
* @brief Set the RTC to the supplied time and date.
37+
* @param[in] timeinfo Pointer to struct tm containing the desired time.
38+
* @return true on success, false otherwise.
39+
*/
40+
bool setTime(const struct tm* timeinfo);
41+
42+
private:
43+
uint8_t bcd2dec(uint8_t val);
44+
uint8_t dec2bcd(uint8_t val);
45+
46+
I2cBus& mBus;
47+
i2c_master_dev_handle_t mDevHandle;
48+
};
49+
50+
#endif // ds3231_h

firmware/main/include/i2c_bus.h

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/******************************************************************************
2+
* File: i2c_bus.h
3+
* Author: Daniel Knezevic
4+
* Year: 2025
5+
* Brief: Object-oriented wrapper around the ESP-IDF I2C master driver
6+
******************************************************************************/
7+
8+
#ifndef i2c_bus_h
9+
#define i2c_bus_h
10+
11+
#include "driver/i2c_master.h"
12+
#include "esp_err.h"
13+
14+
class I2cBus {
15+
public:
16+
/**
17+
* @brief Construct a new I2cBus instance.
18+
*
19+
* @param port I2C hardware port number (e.g., I2C_NUM_0 or I2C_NUM_1).
20+
* @param sda GPIO pin number used for the SDA line.
21+
* @param scl GPIO pin number used for the SCL line.
22+
*/
23+
I2cBus(i2c_port_t port, gpio_num_t sda, gpio_num_t scl);
24+
25+
/**
26+
* @brief Destructor for the I2cBus.
27+
*
28+
* Automatically deletes the I2C master bus if it was initialized.
29+
*/
30+
~I2cBus();
31+
32+
/**
33+
* @brief Initialize the I2C master bus.
34+
*
35+
* Configures the I2C master bus with internal pull-ups and default clock
36+
* source. Must be called before adding devices or performing transactions.
37+
*
38+
* @return
39+
* - ESP_OK on success
40+
* - ESP_ERR_INVALID_ARG if parameters are invalid
41+
* - ESP_ERR_NO_MEM if allocation fails
42+
* - Other error codes from the ESP-IDF I2C driver
43+
*/
44+
esp_err_t initialize();
45+
46+
/**
47+
* @brief Add a device to the I2C bus.
48+
*
49+
* Each device can use its own SCL frequency. The returned device handle
50+
* can be used in subsequent read/write operations.
51+
*
52+
* @param address 7-bit I2C device address.
53+
* @param freq SCL clock frequency in Hz (e.g., 100000 or 400000).
54+
* @param dev Pointer to a handle that receives the device handle.
55+
* @return
56+
* - ESP_OK on success
57+
* - ESP_ERR_INVALID_ARG or ESP_FAIL on failure
58+
*/
59+
esp_err_t addDevice(uint8_t address, uint32_t freq,
60+
i2c_master_dev_handle_t* dev);
61+
62+
/**
63+
* @brief Write a data buffer to an I2C device.
64+
*
65+
* @param dev Device handle obtained from addDevice().
66+
* @param data Pointer to data buffer to transmit.
67+
* @param len Number of bytes to send.
68+
* @param timeout_ms Timeout for the operation, in milliseconds.
69+
* @return
70+
* - ESP_OK on success
71+
* - ESP_FAIL, ESP_ERR_TIMEOUT, or ESP_ERR_INVALID_STATE on failure
72+
*/
73+
esp_err_t write(i2c_master_dev_handle_t dev, const uint8_t* data,
74+
size_t len, uint32_t timeout_ms = 1000);
75+
76+
/**
77+
* @brief Read data from an I2C device, optionally after writing a register
78+
* address.
79+
*
80+
* Performs a combined write-read transaction if @p reg_len > 0.
81+
*
82+
* @param dev Device handle obtained from addDevice().
83+
* @param reg Optional pointer to the register address buffer (can be
84+
* nullptr).
85+
* @param reg_len Length of the register address buffer in bytes (0 for no
86+
* write phase).
87+
* @param data Pointer to buffer for storing received data.
88+
* @param data_len Number of bytes to read.
89+
* @param timeout_ms Timeout for the operation, in milliseconds.
90+
* @return
91+
* - ESP_OK on success
92+
* - ESP_FAIL, ESP_ERR_TIMEOUT, or ESP_ERR_INVALID_STATE on failure
93+
*/
94+
esp_err_t read(i2c_master_dev_handle_t dev, const uint8_t* reg,
95+
size_t reg_len, uint8_t* data, size_t data_len,
96+
uint32_t timeout_ms = 1000);
97+
98+
private:
99+
i2c_port_t mPort;
100+
gpio_num_t mSda;
101+
gpio_num_t mScl;
102+
i2c_master_bus_handle_t mBus;
103+
};
104+
105+
#endif // i2c_bus_h

firmware/main/include/nixie_clock.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
#include "freertos/task.h"
99

1010
#include "clock_iface.h"
11+
#include "ds3231.h"
12+
#include "i2c_bus.h"
1113
#include "in14_nixie_tube.h"
1214
#include "led_controller.h"
1315
#include "sleep_info.h"
@@ -35,11 +37,13 @@ class NixieClock : public IClock {
3537
void setupCaptivePortal();
3638
void startMdnsService(const WifiInfo& wifiInfo);
3739
void initializeSNTP();
40+
static void timeSyncNotificationCallback(struct timeval* tv);
3841
bool isInSleepMode();
3942
static void loopTask(void* param);
4043
bool startShowCurrentTimeTask(void);
4144
static void showCurrentTimeTask(void* param);
4245
void handleSleepMode();
46+
time_t timegmRtc(struct tm* tm);
4347

4448
LedController mLedController;
4549
In14NixieTube mNixieTube;
@@ -48,5 +52,7 @@ class NixieClock : public IClock {
4852
SleepInfo mSleepInfo;
4953
TimeInfo mTimeInfo;
5054
TaskHandle_t mShowCurrentTimeTaskHandle;
55+
I2cBus mI2c;
56+
Ds3231 mRtc;
5157
};
5258
#endif // nixie_clock_h

0 commit comments

Comments
 (0)