diff --git a/drivers/ds3231/Makefile b/drivers/ds3231/Makefile new file mode 100644 index 0000000000..48422e909a --- /dev/null +++ b/drivers/ds3231/Makefile @@ -0,0 +1 @@ +include $(RIOTBASE)/Makefile.base diff --git a/drivers/ds3231/Makefile.dep b/drivers/ds3231/Makefile.dep new file mode 100644 index 0000000000..e67057d463 --- /dev/null +++ b/drivers/ds3231/Makefile.dep @@ -0,0 +1 @@ +FEATURES_REQUIRED += periph_i2c diff --git a/drivers/ds3231/Makefile.include b/drivers/ds3231/Makefile.include new file mode 100644 index 0000000000..7787803bd0 --- /dev/null +++ b/drivers/ds3231/Makefile.include @@ -0,0 +1,2 @@ +USEMODULE_INCLUDES_ds3231 := $(LAST_MAKEFILEDIR)/include +USEMODULE_INCLUDES += $(USEMODULE_INCLUDES_ds3231) diff --git a/drivers/ds3231/ds3231.c b/drivers/ds3231/ds3231.c new file mode 100644 index 0000000000..2d0459c608 --- /dev/null +++ b/drivers/ds3231/ds3231.c @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2020 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser General + * Public License v2.1. See the file LICENSE in the top level directory for more + * details. + */ + +/** + * @ingroup drivers_ds3231 + * @{ + * + * @file + * @brief DS3231 RTC driver implementation + * + * @author Hauke Petersen + * + * @} + */ + +#include + +#include "bcd.h" +#include "ds3231.h" + +#define ENABLE_DEBUG 0 +#include "debug.h" + +/* some metadata about the devices registers */ +#define DATE_REG_NUMOF 7U +#define A1_REG_NUMOF 4U +#define A2_REG_NUMOF 3U + +/* register addresses */ +#define REG_SEC 0x00 +#define REG_MIN 0x01 +#define REG_HOUR 0x02 +#define REG_DAY 0x03 +#define REG_DATE 0x04 +#define REG_MONTH 0x05 +#define REG_YEAR 0x06 +#define REG_A1_SEC 0x07 +#define REG_A1_MIN 0x08 +#define REG_A1_HOUR 0x09 +#define REG_A1_DAYDATE 0x0a +#define REG_A2_MIN 0x0b +#define REG_A2_HOUR 0x0c +#define REG_A2_DAYDATE 0x0d +#define REG_CTRL 0x0e +#define REG_STATUS 0x0f +#define REG_AGING_OFFSET 0x10 +#define REG_TEMP_MSB 0x11 +#define REG_TEMP_LSB 0x12 + +/* general register bitmasks */ +#define MASK_SEC10 0x70 +#define MASK_SEC 0x0f +#define MASK_MIN10 0x70 +#define MASK_MIN 0x0f +#define MASK_H10 0x10 +#define MASK_AMPM_H20 0x20 +#define MASK_H12_H24 0x40 +#define MASK_H20H10 (MASK_H10 | MASK_AMPM_H20) +#define MASK_HOUR 0x0f +#define MASK_DAY 0x07 +#define MASK_DATE10 0x30 +#define MASK_DATE 0x0f +#define MASK_MONTH10 0x10 +#define MASK_MONTH 0x0f +#define MASK_CENTURY 0x80 +#define MASK_DY_DT 0x60 + +/* control register bitmaps */ +#define CTRL_EOSC 0x80 +#define CTRL_BBSQW 0x40 +#define CTRL_CONV 0x20 +#define CTRL_RS2 0x10 +#define CTRL_RS1 0x80 +#define CTRL_RS (CTRL_RS2 | CTRL_RS1) +#define CTRL_INTCN 0x40 +#define CTRL_A2IE 0x20 +#define CTRL_A1IE 0x10 +#define CTRL_AIE (CTRL_A2IE | CTRL_A1IE) + +/* status register bitmaps */ +#define STAT_OSF 0x80 +#define STAT_EN32KHZ 0x08 +#define STAT_BSY 0x04 +#define STAT_A2F 0x02 +#define STAT_A1F 0x01 +#define STAT_AF (STAT_A2F | STAT_A1F) + +static int _read(const ds3231_t *dev, uint8_t reg, uint8_t *buf, size_t len, + int acquire, int release) +{ + int res; + + if (acquire && i2c_acquire(dev->bus)) { + return -EIO; + } + res = i2c_read_regs(dev->bus, DS3231_I2C_ADDR, reg, buf, len, 0); + if (res < 0) { + i2c_release(dev->bus); + return -EIO; + } + if (release) { + i2c_release(dev->bus); + } + return 0; +} + +static int _write(const ds3231_t *dev, uint8_t reg, uint8_t *buf, size_t len, + int acquire, int release) +{ + if (acquire && i2c_acquire(dev->bus)) { + return -EIO; + } + if (i2c_write_regs(dev->bus, DS3231_I2C_ADDR, reg, buf, len, 0) < 0) { + i2c_release(dev->bus); + return -EIO; + } + if (release) { + i2c_release(dev->bus); + } + return 0; +} + +static int _clrset(const ds3231_t *dev, uint8_t reg, + uint8_t clr_mask, uint8_t set_mask, int acquire, int release) +{ + uint8_t old; + uint8_t new; + + if (_read(dev, reg, &old, 1, acquire, 0) < 0) { + return -EIO; + } + new = ((old &= ~clr_mask) | set_mask); + if (_write(dev, reg, &new, 1, 0, release) < 0) { + return -EIO; + } + return 0; +} + +int ds3231_init(ds3231_t *dev, const ds3231_params_t *params) +{ + int res; + + /* write device descriptor */ + memset(dev, 0, sizeof(ds3231_t)); + dev->bus = params->bus; + + /* en or disable 32KHz output */ + if (params->opt & DS2321_OPT_32KHZ_ENABLE) { + res = _clrset(dev, REG_STATUS, 0, STAT_EN32KHZ, 1, 0); + } + else { + res = _clrset(dev, REG_STATUS, STAT_EN32KHZ, 0, 1, 0); + } + if (res != 0) { + return -EIO; + } + + /* disable interrupts and configure backup battery */ + uint8_t clr = (CTRL_A1IE | CTRL_A2IE); + uint8_t set = 0; + /* if configured, start the oscillator */ + if (params->opt & DS3231_OPT_BAT_ENABLE) { + clr |= CTRL_EOSC; + } + else { + set = CTRL_EOSC; + } + + return _clrset(dev, REG_CTRL, clr, set, 0, 1); +} + +int ds3231_get_time(const ds3231_t *dev, struct tm *time) +{ + uint8_t raw[DATE_REG_NUMOF]; + + /* read date registers */ + if (_read(dev, REG_SEC, raw, DATE_REG_NUMOF, 1, 1) < 0) { + return -EIO; + } + + /* convert data to struct tm */ + time->tm_sec = bcd_to_byte(raw[REG_SEC]); + time->tm_min = bcd_to_byte(raw[REG_MIN]); + if (raw[REG_HOUR] & MASK_H12_H24) { + time->tm_hour = bcd_to_byte(raw[REG_HOUR] & (MASK_HOUR | MASK_H10)); + if (raw[REG_HOUR] & MASK_AMPM_H20) { + time->tm_hour += 12; + } + } + else { + time->tm_hour = bcd_to_byte(raw[REG_HOUR] & (MASK_HOUR | MASK_H20H10)); + } + time->tm_mday = bcd_to_byte(raw[REG_DATE]); + time->tm_mon = bcd_to_byte(raw[REG_MONTH] & ~MASK_CENTURY) - 1; + time->tm_year = bcd_to_byte(raw[REG_YEAR]); + if (raw[REG_MONTH] & MASK_CENTURY) { + time->tm_year += 100; + } + time->tm_wday = bcd_to_byte(raw[REG_DAY]) - 1; + + return 0; +} + +int ds3231_set_time(const ds3231_t *dev, const struct tm *time) +{ + uint8_t raw[DATE_REG_NUMOF]; + + /* some validity checks */ + if (time->tm_year > 200) { + return -ERANGE; + } + + /* convert struct tm to raw BDC data */ + raw[REG_SEC] = bcd_from_byte(time->tm_sec); + raw[REG_MIN] = bcd_from_byte(time->tm_min); + /* note: we always set the hours in 24-hour format */ + raw[REG_HOUR] = bcd_from_byte(time->tm_hour); + raw[REG_DAY] = bcd_from_byte(time->tm_wday + 1); + raw[REG_DATE] = bcd_from_byte(time->tm_mday); + raw[REG_MONTH] = bcd_from_byte(time->tm_mon + 1); + raw[REG_YEAR] = bcd_from_byte(time->tm_year % 100); + if (time->tm_year > 100) { + raw[REG_MONTH] |= MASK_CENTURY; + } + + /* write time to device */ + if (_write(dev, REG_SEC, raw, DATE_REG_NUMOF, 1, 1) < 0) { + return -EIO; + } + + return 0; +} + +int ds3231_get_aging_offset(const ds3231_t *dev, int8_t *offset) +{ + return _read(dev, REG_AGING_OFFSET, (uint8_t *)offset, 1, 1, 1); +} + +int ds3231_set_aging_offset(const ds3231_t *dev, int8_t offset) +{ + return _write(dev, REG_AGING_OFFSET, (uint8_t *)&offset, 1, 1, 1); +} + +int ds3231_get_temp(const ds3231_t *dev, int16_t *temp) +{ + uint8_t raw[2]; + int res = _read(dev, REG_TEMP_MSB, raw, 2, 1, 1); + if (res != 0) { + return -EIO; + } + *temp = ((((int16_t)raw[0] << 8) | raw[1]) >> 6) * 25; + return 0; +} + +int ds3231_enable_bat(const ds3231_t *dev) +{ + return _clrset(dev, REG_CTRL, CTRL_EOSC, 0, 1, 1); +} + +int ds3231_disable_bat(const ds3231_t *dev) +{ + return _clrset(dev, REG_CTRL, 0, CTRL_EOSC, 1, 1); +} diff --git a/drivers/ds3231/include/ds3231_params.h b/drivers/ds3231/include/ds3231_params.h new file mode 100644 index 0000000000..f7588aa8c6 --- /dev/null +++ b/drivers/ds3231/include/ds3231_params.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2020 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup drivers_ds3231 + * @{ + * + * @file + * @brief Default configuration for DS3231 devices + * + * @author Hauke Petersen + */ +#ifndef DS3231_PARAMS_H +#define DS3231_PARAMS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @name Default configuration parameters for the DS3231 driver + * @{ + */ +#ifndef DS3231_PARAM_I2C +#define DS3231_PARAM_I2C I2C_DEV(0) +#endif +#ifndef DS3231_PARAM_OPT +#define DS3231_PARAM_OPT (DS3231_OPT_BAT_ENABLE) +#endif + +#ifndef DS3231_PARAMS +#define DS3231_PARAMS { .bus = DS3231_PARAM_I2C, \ + .opt = DS3231_PARAM_OPT, } +#endif +/** @} */ + +/** + * @brief DS3231 configuration + */ +static const ds3231_params_t ds3231_params[] = +{ + DS3231_PARAMS +}; + +#ifdef __cplusplus +} +#endif + +#endif /* DS3231_PARAMS_H */ +/** @} */ diff --git a/drivers/include/ds3231.h b/drivers/include/ds3231.h new file mode 100644 index 0000000000..675a272369 --- /dev/null +++ b/drivers/include/ds3231.h @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2020 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup drivers_ds3231 DS3231 Real Time Clock + * @ingroup drivers_sensors + * @brief Driver for the Maxim DS3231 extremely accurate RTC + * + * # About + * This module implements a device driver for Maxim DS3231 RTC. + * + * # Implementation status + * The current implementation does only support reading and setting of time + * registers as well as reading the temperature register and configuring the + * aging offset. + * + * Setting alarms and configuring the square wave output is not yet supported. + * + * @{ + * @file + * @brief Interface definition for the Maxim DS3231 RTC + * + * @author Hauke Petersen + */ + +#ifndef DS3231_H +#define DS3231_H + +#include +#include + +#include "periph/i2c.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Default address of DS3231 sensors + */ +#define DS3231_I2C_ADDR 0x68 + +/** + * @brief Configuration options + */ +enum { + DS3231_OPT_BAT_ENABLE = 0x01, /* enable backup battery on startup */ + DS2321_OPT_32KHZ_ENABLE = 0x02, /* enable 32KHz output */ +}; + +/** + * @brief Device descriptor for DS3231 devices + */ +typedef struct { + i2c_t bus; /**< I2C bus the device is connected to */ +} ds3231_t; + +/** + * @brief Set of configuration parameters for DS3231 devices + */ +typedef struct { + i2c_t bus; /**< I2C bus the device is connected to */ + uint8_t opt; /**< additional options */ +} ds3231_params_t; + +/** + * @brief Initialize the given DS3231 device + * + * @param[out] dev device descriptor of the targeted device + * @param[in] params device configuration + * + * @return 0 on success + * @return -EIO if no DS3231 device was found + */ +int ds3231_init(ds3231_t *dev, const ds3231_params_t *params); + +/** + * @brief Get date and time from the device + * + * @param[in] dev DS3231 device descriptor + * @param[out] time current date and time from on device + * + * @return 0 on success + * @return -EIO on I2C communication error + */ +int ds3231_get_time(const ds3231_t *dev, struct tm *time); + +/** + * @brief Set date and time of the device + * + * @param[in] dev DS3231 device descriptor + * @param[in] time target date and time + * + * @return 0 on success + * @return -EIO on I2C communication error + */ +int ds3231_set_time(const ds3231_t *dev, const struct tm *time); + +/** + * @brief Get the configured aging offset (see datasheet for more information) + * + * @param[in] dev DS3231 device descriptor + * @param[out] offset aging offset + * + * @return 0 on success + * @return -EIO on I2C communication error + */ +int ds3231_get_aging_offset(const ds3231_t *dev, int8_t *offset); + +/** + * @brief Set the aging offset (see datasheet for more information) + * + * @param[in] dev DS3231 device descriptor + * @param[in] offset aging offset + * + * @return 0 on success + * @return -EIO on I2C communication error + */ +int ds3231_set_aging_offset(const ds3231_t *dev, int8_t offset); + +/** + * @brief Get temperature from the device + * + * @param[in] dev DS3231 device descriptor + * @param[out] temp current value of the temperature register [in centi °C] + * + * @return 0 on success + * @return -EIO on I2C communication error + */ +int ds3231_get_temp(const ds3231_t *dev, int16_t *temp); + +/** + * @brief Enable the backup battery + * + * @param[in] dev DS3231 device descriptor + * + * @return 0 on success + * @return -EIO on I2C communication error + */ +int ds3231_enable_bat(const ds3231_t *dev); + +/** + * @brief Disable the backup battery + * + * @param[in] dev DS3231 device descriptor + * + * @return 0 on success + * @return -EIO on I2C communication error + */ +int ds3231_disable_bat(const ds3231_t *dev); + +#ifdef __cplusplus +} +#endif + +#endif /* DS3231_H */ +/** @} */ diff --git a/tests/driver_ds3231/Makefile b/tests/driver_ds3231/Makefile new file mode 100644 index 0000000000..76db659f0d --- /dev/null +++ b/tests/driver_ds3231/Makefile @@ -0,0 +1,11 @@ +include ../Makefile.tests_common + +# Blacklist iotlab boards since a different device has the same i2c address +BOARD_BLACKLIST := iotlab-a8-m3 \ + iotlab-m3 + +USEMODULE += ds3231 +USEMODULE += xtimer +USEMODULE += shell + +include $(RIOTBASE)/Makefile.include diff --git a/tests/driver_ds3231/Makefile.ci b/tests/driver_ds3231/Makefile.ci new file mode 100644 index 0000000000..6d99904847 --- /dev/null +++ b/tests/driver_ds3231/Makefile.ci @@ -0,0 +1,12 @@ +BOARD_INSUFFICIENT_MEMORY := \ + arduino-duemilanove \ + arduino-leonardo \ + arduino-nano \ + arduino-uno \ + atmega328p \ + nucleo-f031k6 \ + nucleo-f042k6 \ + nucleo-l011k4 \ + stk3200 \ + stm32f030f4-demo \ + # diff --git a/tests/driver_ds3231/main.c b/tests/driver_ds3231/main.c new file mode 100644 index 0000000000..2b4d6a83f4 --- /dev/null +++ b/tests/driver_ds3231/main.c @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2020 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup tests + * @{ + * + * @file + * @brief Test application for the DS3231 RTC driver + * + * @author Hauke Petersen + * + * @} + */ + +#include +#include +#include +#include + +#include "shell.h" +#include "xtimer.h" +#include "ds3231.h" +#include "ds3231_params.h" + +#define ISOSTR_LEN (20U) +#define TEST_DELAY (2U) + +static ds3231_t _dev; + +/* 2010-09-22T15:10:42 is the author date of RIOT's initial commit */ +static struct tm _riot_bday = { + .tm_sec = 42, + .tm_min = 10, + .tm_hour = 15, + .tm_wday = 3, + .tm_mday = 22, + .tm_mon = 8, + .tm_year = 110 +}; + +/* parse ISO date string (YYYY-MM-DDTHH:mm:ss) to struct tm */ +static int _tm_from_str(const char *str, struct tm *time) +{ + char tmp[5]; + + if (strlen(str) != ISOSTR_LEN - 1) { + return -1; + } + if ((str[4] != '-') || (str[7] != '-') || (str[10] != 'T') || + (str[13] != ':') || (str[16] != ':')) { + return -1; + } + + memset(time, 0, sizeof(struct tm)); + + memcpy(tmp, str, 4); + tmp[4] = '\0'; + str += 5; + time->tm_year = atoi(tmp) - 1900; + + memcpy(tmp, str, 2); + tmp[2] = '\0'; + str += 3; + time->tm_mon = atoi(tmp) - 1; + + memcpy(tmp, str, 2); + str += 3; + time->tm_mday = atoi(tmp); + + memcpy(tmp, str, 2); + str += 3; + time->tm_hour = atoi(tmp); + + memcpy(tmp, str, 2); + str += 3; + time->tm_min = atoi(tmp); + + memcpy(tmp, str, 2); + time->tm_sec = atoi(tmp); + + return 0; +} + +static int _cmd_get(int argc, char **argv) +{ + (void)argc; + (void)argv; + char dstr[ISOSTR_LEN]; + + struct tm time; + ds3231_get_time(&_dev, &time); + + size_t pos = strftime(dstr, ISOSTR_LEN, "%Y-%m-%dT%H:%M:%S", &time); + dstr[pos] = '\0'; + printf("The current time is: %s\n", dstr); + + return 0; +} + +static int _cmd_set(int argc, char **argv) +{ + if (argc != 2) { + printf("usage: %s \n", argv[0]); + return 1; + } + + if (strlen(argv[1]) != (ISOSTR_LEN - 1)) { + puts("error: input date string has invalid length"); + return 1; + } + + struct tm target_time; + int res = _tm_from_str(argv[1], &target_time); + if (res != 0) { + puts("error: unable do parse input date string"); + return 1; + } + + ds3231_set_time(&_dev, &target_time); + + printf("success: time set to %s\n", argv[1]); + return 0; +} + +static int _cmd_temp(int argc, char **argv) +{ + (void)argc; + (void)argv; + int16_t temp; + + int res = ds3231_get_temp(&_dev, &temp); + if (res != 0) { + puts("error: unable to read temperature"); + return 1; + } + + int t1 = temp / 100; + int t2 = temp - (t1 * 100); + printf("Current temperature: %i.%02i°C\n", t1, t2); + + return 0; +} + +static int _cmd_aging(int argc, char **argv) +{ + int8_t val; + int res; + + if (argc == 1) { + res = ds3231_get_aging_offset(&_dev, &val); + if (res != 0) { + puts("error: unable to obtain aging offset"); + return 1; + } + printf("Aging offset: %i\n", (int)val); + } + else { + val = atoi(argv[1]); + res = ds3231_set_aging_offset(&_dev, val); + if (res != 0) { + puts("error: unable to set againg offset"); + return 1; + } + printf("Success: set aging offset to %i\n", (int)val); + } + + return 0; +} + +static int _cmd_bat(int argc, char **argv) +{ + int res; + + if (argc != 2) { + printf("usage: %s <'0' or '1'>\n", argv[0]); + return 1; + } + + if (argv[1][0] == '1') { + res = ds3231_enable_bat(&_dev); + if (res == 0) { + puts("success: backup battery enabled"); + } + else { + puts("error: unable to enable backup battery"); + } + } + else if (argv[1][0] == '0') { + res = ds3231_disable_bat(&_dev); + if (res == 0) { + puts("success: backup battery disabled"); + } + else { + puts("error: unable to disable backup battery"); + } + } + else { + puts("error: unable to parse command"); + return 1; + } + + return 0; +} + +static int _cmd_test(int argc, char **argv) +{ + (void)argc; + (void)argv; + int res; + struct tm time; + + puts("testing device now"); + + /* set time to RIOT birthdate */ + res = ds3231_set_time(&_dev, &_riot_bday); + if (res != 0) { + puts("error: unable to set time"); + return 1; + } + + /* read time and compare to initial value */ + res = ds3231_get_time(&_dev, &time); + if (res != 0) { + puts("error: unable to read time"); + return 1; + } + + if ((mktime(&time) - mktime(&_riot_bday)) > 1) { + puts("error: device time has unexpected value"); + return 1; + } + + /* wait a short while and check if time has progressed */ + xtimer_sleep(TEST_DELAY); + res = ds3231_get_time(&_dev, &time); + if (res != 0) { + puts("error: unable to read time"); + return 1; + } + + if (!(mktime(&time) > mktime(&_riot_bday))) { + puts("error: time did not progress"); + return 1; + } + + puts("OK"); + return 0; +} + +static const shell_command_t shell_commands[] = { + { "time_get", "init as output (push-pull mode)", _cmd_get }, + { "time_set", "init as input w/o pull resistor", _cmd_set }, + { "temp", "get temperature", _cmd_temp }, + { "aging", "get or set the aging offset", _cmd_aging }, + { "bat", "en/disable backup battery", _cmd_bat }, + { "test", "test if the device is working properly", _cmd_test}, + { NULL, NULL, NULL } +}; + +int main(void) +{ + int res; + + puts("DS3231 RTC test\n"); + + /* initialize the device */ + res = ds3231_init(&_dev, &ds3231_params[0]); + if (res != 0) { + puts("error: unable to initialize DS3231 [I2C initialization error]"); + return 1; + } + + /* start the shell */ + char line_buf[SHELL_DEFAULT_BUFSIZE]; + shell_run(shell_commands, line_buf, SHELL_DEFAULT_BUFSIZE); + + return 0; +} diff --git a/tests/driver_ds3231/tests/01-run.py b/tests/driver_ds3231/tests/01-run.py new file mode 100755 index 0000000000..b4de92cb73 --- /dev/null +++ b/tests/driver_ds3231/tests/01-run.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2016 Kaspar Schleiser +# +# This file is subject to the terms and conditions of the GNU Lesser +# General Public License v2.1. See the file LICENSE in the top level +# directory for more details. + +import sys +from testrunner import run + + +def testfunc(child): + child.sendline("test") + child.expect_exact("OK") + + +if __name__ == "__main__": + sys.exit(run(testfunc))