| /* |
| * Copyright (c) 2019-2023 Henrik Brix Andersen <henrik@brixandersen.dk> |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #define DT_DRV_COMPAT nxp_pcf8523 |
| |
| #include <zephyr/drivers/gpio.h> |
| #include <zephyr/drivers/i2c.h> |
| #include <zephyr/drivers/rtc.h> |
| #include <zephyr/logging/log.h> |
| #include <zephyr/pm/device.h> |
| #include <zephyr/sys/util.h> |
| |
| LOG_MODULE_REGISTER(pcf8523, CONFIG_RTC_LOG_LEVEL); |
| |
| /* PCF8523 register addresses */ |
| #define PCF8523_CONTROL_1 0x00U |
| #define PCF8523_CONTROL_2 0x01U |
| #define PCF8523_CONTROL_3 0x02U |
| #define PCF8523_SECONDS 0x03U |
| #define PCF8523_MINUTES 0x04U |
| #define PCF8523_HOURS 0x05U |
| #define PCF8523_DAYS 0x06U |
| #define PCF8523_WEEKDAYS 0x07U |
| #define PCF8523_MONTHS 0x08U |
| #define PCF8523_YEARS 0x09U |
| #define PCF8523_MINUTE_ALARM 0x0aU |
| #define PCF8523_HOUR_ALARM 0x0bU |
| #define PCF8523_DAY_ALARM 0x0cU |
| #define PCF8523_WEEKDAY_ALARM 0x0dU |
| #define PCF8523_OFFSET 0x0eU |
| #define PCF8523_TMR_CLKOUT_CTRL 0x0fU |
| #define PCF8523_TMR_A_FREQ_CTRL 0x10U |
| #define PCF8523_TMR_A_REG 0x11U |
| #define PCF8523_TMR_B_FREQ_CTRL 0x12U |
| #define PCF8523_TMR_B_REG 0x13U |
| |
| /* Control register bits */ |
| #define PCF8523_CONTROL_1_CAP_SEL BIT(7) |
| #define PCF8523_CONTROL_1_T BIT(6) |
| #define PCF8523_CONTROL_1_STOP BIT(5) |
| #define PCF8523_CONTROL_1_SR BIT(4) |
| #define PCF8523_CONTROL_1_12_24 BIT(3) |
| #define PCF8523_CONTROL_1_SIE BIT(2) |
| #define PCF8523_CONTROL_1_AIE BIT(1) |
| #define PCF8523_CONTROL_1_CIE BIT(0) |
| #define PCF8523_CONTROL_2_WTAF BIT(7) |
| #define PCF8523_CONTROL_2_CTAF BIT(6) |
| #define PCF8523_CONTROL_2_CTBF BIT(5) |
| #define PCF8523_CONTROL_2_SF BIT(4) |
| #define PCF8523_CONTROL_2_AF BIT(3) |
| #define PCF8523_CONTROL_2_WTAIE BIT(2) |
| #define PCF8523_CONTROL_2_CTAIE BIT(1) |
| #define PCF8523_CONTROL_2_CTBIE BIT(0) |
| #define PCF8523_CONTROL_3_PM_MASK GENMASK(7, 5) |
| #define PCF8523_CONTROL_3_BSF BIT(3) |
| #define PCF8523_CONTROL_3_BLF BIT(2) |
| #define PCF8523_CONTROL_3_BSIE BIT(1) |
| #define PCF8523_CONTROL_3_BLIE BIT(0) |
| |
| /* Time and date register bits */ |
| #define PCF8523_SECONDS_OS BIT(7) |
| #define PCF8523_SECONDS_MASK GENMASK(6, 0) |
| #define PCF8523_MINUTES_MASK GENMASK(6, 0) |
| #define PCF8523_HOURS_AMPM BIT(5) |
| #define PCF8523_HOURS_12H_MASK GENMASK(4, 0) |
| #define PCF8523_HOURS_24H_MASK GENMASK(5, 0) |
| #define PCF8523_DAYS_MASK GENMASK(5, 0) |
| #define PCF8523_WEEKDAYS_MASK GENMASK(2, 0) |
| #define PCF8523_MONTHS_MASK GENMASK(4, 0) |
| #define PCF8523_YEARS_MASK GENMASK(7, 0) |
| |
| /* Alarm register bits */ |
| #define PCF8523_MINUTE_ALARM_AEN_M BIT(7) |
| #define PCF8523_MINUTE_ALARM_MASK GENMASK(6, 0) |
| #define PCF8523_HOUR_ALARM_AEN_H BIT(7) |
| #define PCF8523_HOUR_ALARM_AMPM BIT(5) |
| #define PCF8523_HOUR_ALARM_12H_MASK GENMASK(4, 0) |
| #define PCF8523_HOUR_ALARM_24H_MASK GENMASK(5, 0) |
| #define PCF8523_DAY_ALARM_AEN_D BIT(7) |
| #define PCF8523_DAY_ALARM_MASK GENMASK(5, 0) |
| #define PCF8523_WEEKDAY_ALARM_AEN_W BIT(7) |
| #define PCF8523_WEEKDAY_ALARM_MASK GENMASK(5, 0) |
| |
| /* Timer register bits */ |
| #define PCF8523_TMR_CLKOUT_CTRL_TAM BIT(7) |
| #define PCF8523_TMR_CLKOUT_CTRL_TBM BIT(6) |
| #define PCF8523_TMR_CLKOUT_CTRL_COF_MASK GENMASK(5, 3) |
| #define PCF8523_TMR_CLKOUT_CTRL_TAC_MASK GENMASK(2, 1) |
| #define PCF8523_TMR_CLKOUT_CTRL_TBC BIT(0) |
| #define PCF8523_TMR_A_FREQ_CTRL_TAQ_MASK GENMASK(2, 0) |
| #define PCF8523_TMR_A_REG_T_A_MASK GENMASK(7, 0) |
| #define PCF8523_TMR_B_FREQ_CTRL_TBW_MASK GENMASK(6, 4) |
| #define PCF8523_TMR_B_FREQ_CTRL_TBQ_MASK GENMASK(2, 0) |
| #define PCF8523_TMR_B_REG_T_B_MASK GENMASK(7, 0) |
| |
| /* Offset register bits */ |
| #define PCF8523_OFFSET_MODE BIT(7) |
| #define PCF8523_OFFSET_MASK GENMASK(6, 0) |
| |
| /* RTC alarm time fields supported by the PCF8523 */ |
| #define PCF8523_RTC_ALARM_TIME_MASK \ |
| (RTC_ALARM_TIME_MASK_MINUTE | RTC_ALARM_TIME_MASK_HOUR | RTC_ALARM_TIME_MASK_MONTHDAY | \ |
| RTC_ALARM_TIME_MASK_WEEKDAY) |
| |
| /* The PCF8523 only supports two-digit years, calculate offset to use */ |
| #define PCF8523_YEARS_OFFSET (2000 - 1900) |
| |
| /* The PCF8523 enumerates months 1 to 12, RTC API uses 0 to 11 */ |
| #define PCF8523_MONTHS_OFFSET 1 |
| |
| /* Helper macro to guard int1-gpios related code */ |
| #if DT_ANY_INST_HAS_PROP_STATUS_OKAY(int1_gpios) && \ |
| (defined(CONFIG_RTC_ALARM) || defined(CONFIG_RTC_UPDATE)) |
| #define PCF8523_INT1_GPIOS_IN_USE 1 |
| #endif |
| |
| struct pcf8523_config { |
| const struct i2c_dt_spec i2c; |
| #ifdef PCF8523_INT1_GPIOS_IN_USE |
| struct gpio_dt_spec int1; |
| #endif /* PCF8523_INT1_GPIOS_IN_USE */ |
| uint8_t cof; |
| uint8_t pm; |
| bool cap_sel; |
| bool wakeup_source; |
| }; |
| |
| struct pcf8523_data { |
| struct k_mutex lock; |
| #if PCF8523_INT1_GPIOS_IN_USE |
| struct gpio_callback int1_callback; |
| struct k_thread int1_thread; |
| struct k_sem int1_sem; |
| |
| K_KERNEL_STACK_MEMBER(int1_stack, CONFIG_RTC_PCF8523_THREAD_STACK_SIZE); |
| #ifdef CONFIG_RTC_ALARM |
| rtc_alarm_callback alarm_callback; |
| void *alarm_user_data; |
| #endif /* CONFIG_RTC_ALARM */ |
| #ifdef CONFIG_RTC_UPDATE |
| rtc_update_callback update_callback; |
| void *update_user_data; |
| #endif /* CONFIG_RTC_UPDATE */ |
| #endif /* PCF8523_INT1_GPIOS_IN_USE */ |
| }; |
| |
| static int pcf8523_read_regs(const struct device *dev, uint8_t addr, void *buf, size_t len) |
| { |
| const struct pcf8523_config *config = dev->config; |
| int err; |
| |
| err = i2c_write_read_dt(&config->i2c, &addr, sizeof(addr), buf, len); |
| if (err != 0) { |
| LOG_ERR("failed to read reg addr 0x%02x, len %d (err %d)", addr, len, err); |
| return err; |
| } |
| |
| return 0; |
| } |
| |
| static int pcf8523_read_reg8(const struct device *dev, uint8_t addr, uint8_t *val) |
| { |
| return pcf8523_read_regs(dev, addr, val, sizeof(*val)); |
| } |
| |
| static int pcf8523_write_regs(const struct device *dev, uint8_t addr, void *buf, size_t len) |
| { |
| const struct pcf8523_config *config = dev->config; |
| uint8_t block[sizeof(addr) + len]; |
| int err; |
| |
| block[0] = addr; |
| memcpy(&block[1], buf, len); |
| |
| err = i2c_write_dt(&config->i2c, block, sizeof(block)); |
| if (err != 0) { |
| LOG_ERR("failed to write reg addr 0x%02x, len %d (err %d)", addr, len, err); |
| return err; |
| } |
| |
| return 0; |
| } |
| |
| static int pcf8523_write_reg8(const struct device *dev, uint8_t addr, uint8_t val) |
| { |
| return pcf8523_write_regs(dev, addr, &val, sizeof(val)); |
| } |
| |
| static int pcf8523_write_stop_bit_unlocked(const struct device *dev, bool value) |
| { |
| uint8_t control_1; |
| int err; |
| |
| err = pcf8523_read_reg8(dev, PCF8523_CONTROL_1, &control_1); |
| if (err != 0) { |
| return err; |
| } |
| |
| if (value) { |
| control_1 |= PCF8523_CONTROL_1_STOP; |
| } else { |
| control_1 &= ~(PCF8523_CONTROL_1_STOP); |
| } |
| |
| err = pcf8523_write_reg8(dev, PCF8523_CONTROL_1, control_1); |
| if (err != 0) { |
| return err; |
| } |
| |
| return 0; |
| } |
| |
| #if PCF8523_INT1_GPIOS_IN_USE |
| static int pcf8523_int1_enable_unlocked(const struct device *dev, bool enable) |
| { |
| const struct pcf8523_config *config = dev->config; |
| uint8_t tmr_clkout_ctrl; |
| int err; |
| |
| if (!config->wakeup_source) { |
| /* Only change COF if not configured as wakeup-source */ |
| err = pcf8523_read_reg8(dev, PCF8523_TMR_CLKOUT_CTRL, &tmr_clkout_ctrl); |
| if (err != 0) { |
| return err; |
| } |
| |
| if (enable) { |
| /* Disable CLKOUT */ |
| tmr_clkout_ctrl |= PCF8523_TMR_CLKOUT_CTRL_COF_MASK; |
| } else if (!config->wakeup_source) { |
| /* Enable CLKOUT */ |
| tmr_clkout_ctrl &= ~(PCF8523_TMR_CLKOUT_CTRL_COF_MASK); |
| tmr_clkout_ctrl |= |
| FIELD_PREP(PCF8523_TMR_CLKOUT_CTRL_COF_MASK, config->cof); |
| } |
| |
| err = pcf8523_write_reg8(dev, PCF8523_TMR_CLKOUT_CTRL, tmr_clkout_ctrl); |
| if (err != 0) { |
| return err; |
| } |
| } |
| |
| /* Use edge interrupts to avoid multiple GPIO IRQs while servicing the IRQ in the thread */ |
| err = gpio_pin_interrupt_configure_dt(&config->int1, |
| enable ? GPIO_INT_EDGE_TO_ACTIVE : GPIO_INT_DISABLE); |
| if (err != 0) { |
| LOG_ERR("failed to %s GPIO IRQ (err %d)", enable ? "enable" : "disable", err); |
| return err; |
| } |
| |
| return 0; |
| } |
| |
| static void pcf8523_int1_thread(void *p1, void *p2, void *p3) |
| { |
| ARG_UNUSED(p2); |
| ARG_UNUSED(p3); |
| |
| const struct device *dev = p1; |
| struct pcf8523_data *data = dev->data; |
| rtc_alarm_callback alarm_callback = NULL; |
| void *alarm_user_data = NULL; |
| rtc_update_callback update_callback = NULL; |
| void *update_user_data = NULL; |
| uint8_t control_2; |
| int err; |
| |
| while (true) { |
| k_sem_take(&data->int1_sem, K_FOREVER); |
| k_mutex_lock(&data->lock, K_FOREVER); |
| |
| err = pcf8523_read_reg8(dev, PCF8523_CONTROL_2, &control_2); |
| if (err != 0) { |
| goto unlock; |
| } |
| |
| #ifdef CONFIG_RTC_ALARM |
| if ((control_2 & PCF8523_CONTROL_2_AF) != 0 && data->alarm_callback != NULL) { |
| control_2 &= ~(PCF8523_CONTROL_2_AF); |
| alarm_callback = data->alarm_callback; |
| alarm_user_data = data->alarm_user_data; |
| } |
| #endif /* CONFIG_RTC_ALARM */ |
| |
| #ifdef CONFIG_RTC_UPDATE |
| if ((control_2 & PCF8523_CONTROL_2_SF) != 0) { |
| control_2 &= ~(PCF8523_CONTROL_2_SF); |
| update_callback = data->update_callback; |
| update_user_data = data->update_user_data; |
| } |
| #endif /* CONFIG_RTC_UPDATE */ |
| |
| control_2 |= PCF8523_CONTROL_2_CTAF | PCF8523_CONTROL_2_CTBF; |
| |
| err = pcf8523_write_reg8(dev, PCF8523_CONTROL_2, control_2); |
| if (err != 0) { |
| goto unlock; |
| } |
| |
| /* Check if interrupt occurred between CONTROL_2 read/write */ |
| err = pcf8523_read_reg8(dev, PCF8523_CONTROL_2, &control_2); |
| if (err != 0) { |
| goto unlock; |
| } |
| |
| if (((control_2 & PCF8523_CONTROL_2_AF) != 0U && alarm_callback != NULL) || |
| ((control_2 & PCF8523_CONTROL_2_SF) != 0U)) { |
| /* |
| * Another interrupt occurred while servicing this one, process current |
| * callback(s) and yield. |
| */ |
| k_sem_give(&data->int1_sem); |
| } |
| |
| unlock: |
| k_mutex_unlock(&data->lock); |
| |
| if (alarm_callback != NULL) { |
| alarm_callback(dev, 0U, alarm_user_data); |
| alarm_callback = NULL; |
| } |
| |
| if (update_callback != NULL) { |
| update_callback(dev, update_user_data); |
| update_callback = NULL; |
| } |
| } |
| } |
| |
| static void pcf8523_int1_callback_handler(const struct device *port, struct gpio_callback *cb, |
| gpio_port_pins_t pins) |
| { |
| struct pcf8523_data *data = CONTAINER_OF(cb, struct pcf8523_data, int1_callback); |
| |
| ARG_UNUSED(port); |
| ARG_UNUSED(pins); |
| |
| k_sem_give(&data->int1_sem); |
| } |
| #endif /* PCF8523_INT1_GPIOS_IN_USE */ |
| |
| static int pcf8523_set_time(const struct device *dev, const struct rtc_time *timeptr) |
| { |
| struct pcf8523_data *data = dev->data; |
| uint8_t regs[7]; |
| int err; |
| |
| if (timeptr->tm_year < PCF8523_YEARS_OFFSET || |
| timeptr->tm_year > PCF8523_YEARS_OFFSET + 99) { |
| return -EINVAL; |
| } |
| |
| k_mutex_lock(&data->lock, K_FOREVER); |
| |
| /* Freeze the time circuits */ |
| err = pcf8523_write_stop_bit_unlocked(dev, true); |
| if (err != 0) { |
| goto unlock; |
| } |
| |
| LOG_DBG("set time: year = %d, mon = %d, mday = %d, wday = %d, hour = %d, " |
| "min = %d, sec = %d", |
| timeptr->tm_year, timeptr->tm_mon, timeptr->tm_mday, timeptr->tm_wday, |
| timeptr->tm_hour, timeptr->tm_min, timeptr->tm_sec); |
| |
| regs[0] = bin2bcd(timeptr->tm_sec) & PCF8523_SECONDS_MASK; |
| regs[1] = bin2bcd(timeptr->tm_min) & PCF8523_MINUTES_MASK; |
| regs[2] = bin2bcd(timeptr->tm_hour) & PCF8523_HOURS_24H_MASK; |
| regs[3] = bin2bcd(timeptr->tm_mday) & PCF8523_DAYS_MASK; |
| regs[4] = bin2bcd(timeptr->tm_wday) & PCF8523_WEEKDAYS_MASK; |
| regs[5] = bin2bcd(timeptr->tm_mon + PCF8523_MONTHS_OFFSET) & PCF8523_MONTHS_MASK; |
| regs[6] = bin2bcd(timeptr->tm_year - PCF8523_YEARS_OFFSET) & PCF8523_YEARS_MASK; |
| |
| /* Write registers PCF8523_SECONDS through PCF8523_YEARS */ |
| err = pcf8523_write_regs(dev, PCF8523_SECONDS, ®s, sizeof(regs)); |
| if (err != 0) { |
| goto unlock; |
| } |
| |
| /* Unfreeze time circuits */ |
| err = pcf8523_write_stop_bit_unlocked(dev, false); |
| if (err != 0) { |
| goto unlock; |
| } |
| |
| unlock: |
| k_mutex_unlock(&data->lock); |
| |
| return err; |
| } |
| |
| static int pcf8523_get_time(const struct device *dev, struct rtc_time *timeptr) |
| { |
| uint8_t regs[10]; |
| int err; |
| |
| /* Read registers PCF8523_CONTROL_1 through PCF8523_YEARS */ |
| err = pcf8523_read_regs(dev, PCF8523_CONTROL_1, ®s, sizeof(regs)); |
| if (err != 0) { |
| return err; |
| } |
| |
| if ((regs[0] & PCF8523_CONTROL_1_STOP) != 0) { |
| LOG_WRN("time circuits frozen"); |
| return -ENODATA; |
| } |
| |
| if ((regs[3] & PCF8523_SECONDS_OS) != 0) { |
| LOG_WRN("oscillator stopped or interrupted"); |
| return -ENODATA; |
| } |
| |
| memset(timeptr, 0U, sizeof(*timeptr)); |
| timeptr->tm_sec = bcd2bin(regs[3] & PCF8523_SECONDS_MASK); |
| timeptr->tm_min = bcd2bin(regs[4] & PCF8523_MINUTES_MASK); |
| timeptr->tm_hour = bcd2bin(regs[5] & PCF8523_HOURS_24H_MASK); |
| timeptr->tm_mday = bcd2bin(regs[6] & PCF8523_DAYS_MASK); |
| timeptr->tm_wday = bcd2bin(regs[7] & PCF8523_WEEKDAYS_MASK); |
| timeptr->tm_mon = bcd2bin(regs[8] & PCF8523_MONTHS_MASK) - PCF8523_MONTHS_OFFSET; |
| timeptr->tm_year = bcd2bin(regs[9] & PCF8523_YEARS_MASK) + PCF8523_YEARS_OFFSET; |
| timeptr->tm_yday = -1; |
| timeptr->tm_isdst = -1; |
| |
| LOG_DBG("get time: year = %d, mon = %d, mday = %d, wday = %d, hour = %d, " |
| "min = %d, sec = %d", |
| timeptr->tm_year, timeptr->tm_mon, timeptr->tm_mday, timeptr->tm_wday, |
| timeptr->tm_hour, timeptr->tm_min, timeptr->tm_sec); |
| |
| return 0; |
| } |
| |
| #ifdef CONFIG_RTC_ALARM |
| static int pcf8523_alarm_get_supported_fields(const struct device *dev, uint16_t id, uint16_t *mask) |
| { |
| ARG_UNUSED(dev); |
| |
| if (id != 0U) { |
| LOG_ERR("invalid ID %d", id); |
| return -EINVAL; |
| } |
| |
| *mask = PCF8523_RTC_ALARM_TIME_MASK; |
| |
| return 0; |
| } |
| |
| static int pcf8523_alarm_set_time(const struct device *dev, uint16_t id, uint16_t mask, |
| const struct rtc_time *timeptr) |
| { |
| uint8_t regs[4]; |
| |
| if (id != 0U) { |
| LOG_ERR("invalid ID %d", id); |
| return -EINVAL; |
| } |
| |
| if ((mask & ~(PCF8523_RTC_ALARM_TIME_MASK)) != 0U) { |
| LOG_ERR("unsupported alarm field mask 0x%04x", mask); |
| return -EINVAL; |
| } |
| |
| if ((mask & RTC_ALARM_TIME_MASK_MINUTE) != 0U) { |
| regs[0] = bin2bcd(timeptr->tm_min) & PCF8523_MINUTE_ALARM_MASK; |
| } else { |
| regs[0] = PCF8523_MINUTE_ALARM_AEN_M; |
| } |
| |
| if ((mask & RTC_ALARM_TIME_MASK_HOUR) != 0U) { |
| regs[1] = bin2bcd(timeptr->tm_hour) & PCF8523_HOUR_ALARM_24H_MASK; |
| } else { |
| regs[1] = PCF8523_HOUR_ALARM_AEN_H; |
| } |
| |
| if ((mask & RTC_ALARM_TIME_MASK_MONTHDAY) != 0U) { |
| regs[2] = bin2bcd(timeptr->tm_mday) & PCF8523_DAY_ALARM_MASK; |
| } else { |
| regs[2] = PCF8523_DAY_ALARM_AEN_D; |
| } |
| |
| if ((mask & RTC_ALARM_TIME_MASK_WEEKDAY) != 0U) { |
| regs[3] = bin2bcd(timeptr->tm_wday) & PCF8523_WEEKDAY_ALARM_MASK; |
| } else { |
| regs[3] = PCF8523_WEEKDAY_ALARM_AEN_W; |
| } |
| |
| LOG_DBG("set alarm: year = %d, mon = %d, mday = %d, hour = %d, min = %d, mask = 0x%04x", |
| timeptr->tm_year, timeptr->tm_mon, timeptr->tm_mday, timeptr->tm_hour, |
| timeptr->tm_min, mask); |
| |
| /* Write registers PCF8523_MINUTE_ALARM through PCF8523_WEEKDAY_ALARM */ |
| return pcf8523_write_regs(dev, PCF8523_MINUTE_ALARM, ®s, sizeof(regs)); |
| } |
| |
| static int pcf8523_alarm_get_time(const struct device *dev, uint16_t id, uint16_t *mask, |
| struct rtc_time *timeptr) |
| { |
| uint8_t regs[4]; |
| int err; |
| |
| if (id != 0U) { |
| LOG_ERR("invalid ID %d", id); |
| return -EINVAL; |
| } |
| |
| /* Read registers PCF8523_MINUTE_ALARM through PCF8523_WEEKDAY_ALARM */ |
| err = pcf8523_read_regs(dev, PCF8523_MINUTE_ALARM, ®s, sizeof(regs)); |
| if (err != 0) { |
| return err; |
| } |
| |
| memset(timeptr, 0U, sizeof(*timeptr)); |
| *mask = 0U; |
| |
| if ((regs[0] & PCF8523_MINUTE_ALARM_AEN_M) == 0) { |
| timeptr->tm_min = bcd2bin(regs[0] & PCF8523_MINUTE_ALARM_MASK); |
| *mask |= RTC_ALARM_TIME_MASK_MINUTE; |
| } |
| |
| if ((regs[1] & PCF8523_HOUR_ALARM_AEN_H) == 0) { |
| timeptr->tm_hour = bcd2bin(regs[1] & PCF8523_HOUR_ALARM_24H_MASK); |
| *mask |= RTC_ALARM_TIME_MASK_HOUR; |
| } |
| |
| if ((regs[2] & PCF8523_DAY_ALARM_AEN_D) == 0) { |
| timeptr->tm_mday = bcd2bin(regs[2] & PCF8523_DAY_ALARM_MASK); |
| *mask |= RTC_ALARM_TIME_MASK_MONTHDAY; |
| } |
| |
| if ((regs[3] & PCF8523_WEEKDAY_ALARM_AEN_W) == 0) { |
| timeptr->tm_wday = bcd2bin(regs[3] & PCF8523_WEEKDAY_ALARM_MASK); |
| *mask |= RTC_ALARM_TIME_MASK_WEEKDAY; |
| } |
| |
| LOG_DBG("get alarm: year = %d, mon = %d, mday = %d, hour = %d, min = %d, mask = 0x%04x", |
| timeptr->tm_year, timeptr->tm_mon, timeptr->tm_mday, timeptr->tm_hour, |
| timeptr->tm_min, *mask); |
| |
| return 0; |
| } |
| |
| static int pcf8523_alarm_is_pending(const struct device *dev, uint16_t id) |
| { |
| struct pcf8523_data *data = dev->data; |
| uint8_t control_2; |
| int err; |
| |
| if (id != 0U) { |
| LOG_ERR("invalid ID %d", id); |
| return -EINVAL; |
| } |
| |
| k_mutex_lock(&data->lock, K_FOREVER); |
| |
| err = pcf8523_read_reg8(dev, PCF8523_CONTROL_2, &control_2); |
| if (err != 0) { |
| goto unlock; |
| } |
| |
| if ((control_2 & PCF8523_CONTROL_2_AF) != 0) { |
| /* Clear alarm flag */ |
| control_2 &= ~(PCF8523_CONTROL_2_AF); |
| |
| /* Ensure other flags are left unchanged (PCF8523 performs logic AND at write) */ |
| control_2 |= PCF8523_CONTROL_2_CTAF | PCF8523_CONTROL_2_CTBF | PCF8523_CONTROL_2_SF; |
| |
| err = pcf8523_write_reg8(dev, PCF8523_CONTROL_2, control_2); |
| if (err != 0) { |
| goto unlock; |
| } |
| |
| /* Alarm pending */ |
| err = 1; |
| } |
| |
| unlock: |
| k_mutex_unlock(&data->lock); |
| |
| return err; |
| } |
| |
| #if PCF8523_INT1_GPIOS_IN_USE |
| static int pcf8523_alarm_set_callback(const struct device *dev, uint16_t id, |
| rtc_alarm_callback callback, void *user_data) |
| { |
| const struct pcf8523_config *config = dev->config; |
| struct pcf8523_data *data = dev->data; |
| uint8_t control_1; |
| int err = 0; |
| |
| if (config->int1.port == NULL) { |
| return -ENOTSUP; |
| } |
| |
| if (id != 0U) { |
| LOG_ERR("invalid ID %d", id); |
| return -EINVAL; |
| } |
| |
| k_mutex_lock(&data->lock, K_FOREVER); |
| |
| data->alarm_callback = callback; |
| data->alarm_user_data = user_data; |
| |
| if (!config->wakeup_source) { |
| /* Only change AIE if not configured as wakeup-source */ |
| err = pcf8523_read_reg8(dev, PCF8523_CONTROL_1, &control_1); |
| if (err != 0) { |
| goto unlock; |
| } |
| |
| if (callback != NULL) { |
| control_1 |= PCF8523_CONTROL_1_AIE; |
| } else { |
| control_1 &= ~(PCF8523_CONTROL_1_AIE); |
| } |
| |
| if ((control_1 & PCF8523_CONTROL_1_SIE) == 0U) { |
| /* Only change INT1 GPIO if seconds timer interrupt not enabled */ |
| err = pcf8523_int1_enable_unlocked(dev, callback != NULL); |
| if (err != 0) { |
| goto unlock; |
| } |
| } |
| |
| err = pcf8523_write_reg8(dev, PCF8523_CONTROL_1, control_1); |
| if (err != 0) { |
| goto unlock; |
| } |
| } |
| |
| unlock: |
| k_mutex_unlock(&data->lock); |
| |
| /* Wake up the INT1 thread since the alarm flag may already be set */ |
| k_sem_give(&data->int1_sem); |
| |
| return err; |
| } |
| #endif /* PCF8523_INT1_GPIOS_IN_USE */ |
| #endif /* CONFIG_RTC_ALARM */ |
| |
| #if PCF8523_INT1_GPIOS_IN_USE && defined(CONFIG_RTC_UPDATE) |
| static int pcf8523_update_set_callback(const struct device *dev, rtc_update_callback callback, |
| void *user_data) |
| { |
| const struct pcf8523_config *config = dev->config; |
| struct pcf8523_data *data = dev->data; |
| uint8_t control_1; |
| int err; |
| |
| if (config->int1.port == NULL) { |
| return -ENOTSUP; |
| } |
| |
| k_mutex_lock(&data->lock, K_FOREVER); |
| |
| data->update_callback = callback; |
| data->update_user_data = user_data; |
| |
| err = pcf8523_read_reg8(dev, PCF8523_CONTROL_1, &control_1); |
| if (err != 0) { |
| goto unlock; |
| } |
| |
| if (callback != NULL) { |
| control_1 |= PCF8523_CONTROL_1_SIE; |
| } else { |
| control_1 &= ~(PCF8523_CONTROL_1_SIE); |
| } |
| |
| if ((control_1 & PCF8523_CONTROL_1_AIE) == 0U) { |
| /* Only change INT1 GPIO if alarm interrupt not enabled */ |
| err = pcf8523_int1_enable_unlocked(dev, callback != NULL); |
| if (err != 0) { |
| goto unlock; |
| } |
| } |
| |
| err = pcf8523_write_reg8(dev, PCF8523_CONTROL_1, control_1); |
| if (err != 0) { |
| goto unlock; |
| } |
| |
| unlock: |
| k_mutex_unlock(&data->lock); |
| |
| /* Wake up the INT1 thread since the seconds flag may already be set */ |
| k_sem_give(&data->int1_sem); |
| |
| return err; |
| } |
| #endif /* PCF8523_INT1_GPIOS_IN_USE && defined(CONFIG_RTC_UPDATE) */ |
| |
| #ifdef CONFIG_RTC_CALIBRATION |
| |
| /* See PCF8523 data sheet table 29 */ |
| #if defined(CONFIG_RTC_PCF8523_OFFSET_MODE_SLOW) |
| #define PCF8523_OFFSET_PPB_PER_LSB 4340 |
| #elif defined(CONFIG_RTC_PCF8523_OFFSET_MODE_FAST) |
| #define PCF8523_OFFSET_PPB_PER_LSB 4069 |
| #else |
| #error Unsupported offset mode |
| #endif |
| |
| #define PCF8523_OFFSET_PPB_MIN (-64 * PCF8523_OFFSET_PPB_PER_LSB) |
| #define PCF8523_OFFSET_PPB_MAX (63 * PCF8523_OFFSET_PPB_PER_LSB) |
| |
| static int pcf8523_set_calibration(const struct device *dev, int32_t freq_ppb) |
| { |
| int32_t period_ppb = freq_ppb * -1; |
| int8_t offset; |
| |
| if (period_ppb < PCF8523_OFFSET_PPB_MIN || period_ppb > PCF8523_OFFSET_PPB_MAX) { |
| LOG_WRN("calibration value (%d ppb) out of range", freq_ppb); |
| return -EINVAL; |
| } |
| |
| offset = period_ppb / PCF8523_OFFSET_PPB_PER_LSB; |
| |
| if (IS_ENABLED(CONFIG_RTC_PCF8523_OFFSET_MODE_FAST)) { |
| offset |= PCF8523_OFFSET_MODE; |
| } |
| |
| LOG_DBG("freq_ppb = %d, period_ppb = %d, offset = %d", freq_ppb, period_ppb, offset); |
| |
| return pcf8523_write_reg8(dev, PCF8523_OFFSET, offset); |
| } |
| |
| static int pcf8523_get_calibration(const struct device *dev, int32_t *freq_ppb) |
| { |
| int32_t period_ppb; |
| int8_t offset; |
| int err; |
| |
| err = pcf8523_read_reg8(dev, PCF8523_OFFSET, &offset); |
| if (err != 0) { |
| return err; |
| } |
| |
| /* Clear mode bit and sign extend the offset */ |
| period_ppb = (offset << 1U) >> 1U; |
| |
| period_ppb = period_ppb * PCF8523_OFFSET_PPB_PER_LSB; |
| *freq_ppb = period_ppb * -1; |
| |
| LOG_DBG("freq_ppb = %d, period_ppb = %d, offset = %d", *freq_ppb, period_ppb, offset); |
| |
| return 0; |
| } |
| #endif /* CONFIG_RTC_CALIBRATION */ |
| |
| static int pcf8523_init(const struct device *dev) |
| { |
| const struct pcf8523_config *config = dev->config; |
| struct pcf8523_data *data = dev->data; |
| uint8_t tmr_clkout_ctrl; |
| uint8_t regs[3]; |
| int err; |
| |
| k_mutex_init(&data->lock); |
| |
| if (!i2c_is_ready_dt(&config->i2c)) { |
| LOG_ERR("I2C bus not ready"); |
| return -ENODEV; |
| } |
| |
| #if PCF8523_INT1_GPIOS_IN_USE |
| k_tid_t tid; |
| |
| if (config->int1.port != NULL) { |
| k_sem_init(&data->int1_sem, 0, INT_MAX); |
| |
| if (!gpio_is_ready_dt(&config->int1)) { |
| LOG_ERR("GPIO not ready"); |
| return -ENODEV; |
| } |
| |
| err = gpio_pin_configure_dt(&config->int1, GPIO_INPUT); |
| if (err != 0) { |
| LOG_ERR("failed to configure GPIO (err %d)", err); |
| return -ENODEV; |
| } |
| |
| gpio_init_callback(&data->int1_callback, pcf8523_int1_callback_handler, |
| BIT(config->int1.pin)); |
| |
| err = gpio_add_callback_dt(&config->int1, &data->int1_callback); |
| if (err != 0) { |
| LOG_ERR("failed to add GPIO callback (err %d)", err); |
| return -ENODEV; |
| } |
| |
| tid = k_thread_create(&data->int1_thread, data->int1_stack, |
| K_THREAD_STACK_SIZEOF(data->int1_stack), |
| pcf8523_int1_thread, (void *)dev, NULL, |
| NULL, CONFIG_RTC_PCF8523_THREAD_PRIO, 0, K_NO_WAIT); |
| k_thread_name_set(tid, "pcf8523"); |
| |
| /* |
| * Defer GPIO interrupt configuration due to INT1/CLKOUT pin sharing. This allows |
| * using the CLKOUT square-wave signal for RTC calibration when no alarm/update |
| * callbacks are enabled (and not configured as a wakeup-source). |
| */ |
| } |
| #endif /* PCF8523_INT1_GPIOS_IN_USE */ |
| |
| /* |
| * Manually initialize the required PCF8523 registers as performing a software reset will |
| * reset the time circuits. |
| */ |
| |
| /* Read registers PCF8523_CONTROL_1 through PCF8523_CONTROL_3 */ |
| err = pcf8523_read_regs(dev, PCF8523_CONTROL_1, ®s, sizeof(regs)); |
| if (err != 0) { |
| return -ENODEV; |
| } |
| |
| /* Set quartz crystal load capacitance */ |
| if (config->cap_sel) { |
| regs[0] |= PCF8523_CONTROL_1_CAP_SEL; |
| } else { |
| regs[0] &= ~(PCF8523_CONTROL_1_CAP_SEL); |
| } |
| |
| /* Use 24h time format */ |
| regs[0] &= ~(PCF8523_CONTROL_1_12_24); |
| |
| /* Disable second, alarm, and correction interrupts */ |
| regs[0] &= ~(PCF8523_CONTROL_1_SIE | PCF8523_CONTROL_1_AIE | PCF8523_CONTROL_1_CIE); |
| |
| if (config->wakeup_source) { |
| /* |
| * Always set AIE if wakeup-source. This allows the RTC to wake up the system even |
| * if the INT1 interrupt output is not directly connected to a GPIO (i.e. if |
| * connected to a PMIC input). |
| */ |
| regs[0] |= PCF8523_CONTROL_1_AIE; |
| } |
| |
| /* Clear interrupt flags (except alarm flag, as a wake-up alarm may be pending) */ |
| regs[1] &= ~(PCF8523_CONTROL_2_CTAF | PCF8523_CONTROL_2_CTBF | PCF8523_CONTROL_2_SF); |
| |
| /* Disable watchdog, timer A, and timer B interrupts */ |
| regs[1] &= ~(PCF8523_CONTROL_2_WTAIE | PCF8523_CONTROL_2_CTAIE | PCF8523_CONTROL_2_CTBIE); |
| |
| /* Configure battery switch-over function */ |
| regs[2] &= ~(PCF8523_CONTROL_3_PM_MASK); |
| regs[2] |= FIELD_PREP(PCF8523_CONTROL_3_PM_MASK, config->pm); |
| |
| /* Clear battery status interrupt flag */ |
| regs[2] &= ~(PCF8523_CONTROL_3_BSF); |
| |
| /* Disable battery status interrupts */ |
| regs[2] &= ~(PCF8523_CONTROL_3_BSIE | PCF8523_CONTROL_3_BLIE); |
| |
| /* Write registers PCF8523_CONTROL_1 through PCF8523_CONTROL_3 */ |
| err = pcf8523_write_regs(dev, PCF8523_CONTROL_1, ®s, sizeof(regs)); |
| if (err != 0) { |
| return -ENODEV; |
| } |
| |
| /* Disable watchdog and countdown timers, configure IRQ level*/ |
| tmr_clkout_ctrl = 0U; |
| |
| if (config->wakeup_source) { |
| /* Disable CLKOUT */ |
| tmr_clkout_ctrl |= PCF8523_TMR_CLKOUT_CTRL_COF_MASK; |
| } else { |
| /* Configure CLKOUT frequency */ |
| tmr_clkout_ctrl |= FIELD_PREP(PCF8523_TMR_CLKOUT_CTRL_COF_MASK, config->cof); |
| } |
| |
| err = pcf8523_write_reg8(dev, PCF8523_TMR_CLKOUT_CTRL, tmr_clkout_ctrl); |
| if (err != 0) { |
| return -ENODEV; |
| } |
| |
| return 0; |
| } |
| |
| /* Mapping from DT battery-switch-over enum to CONTROL_3 PM field value */ |
| #define PCF8523_PM_STANDARD 4U |
| #define PCF8523_PM_DIRECT 5U |
| #define PCF8523_PM_DISABLED 7U |
| |
| #ifdef CONFIG_PM_DEVICE |
| static int pcf8523_pm_action(const struct device *dev, enum pm_device_action action) |
| { |
| const struct pcf8523_config *config = dev->config; |
| uint8_t control_3; |
| int err; |
| |
| if (config->pm == PCF8523_PM_DISABLED) { |
| /* Only one power supply */ |
| return -ENOTSUP; |
| } |
| |
| switch (action) { |
| case PM_DEVICE_ACTION_SUSPEND: |
| /* Disable battery switch-over function */ |
| control_3 = FIELD_PREP(PCF8523_CONTROL_3_PM_MASK, PCF8523_PM_DISABLED); |
| break; |
| case PM_DEVICE_ACTION_RESUME: |
| /* Re-enable battery switch-over function */ |
| control_3 = FIELD_PREP(PCF8523_CONTROL_3_PM_MASK, config->pm); |
| break; |
| default: |
| return -ENOTSUP; |
| } |
| |
| err = pcf8523_write_reg8(dev, PCF8523_CONTROL_3, control_3); |
| if (err != 0) { |
| return -EIO; |
| } |
| |
| return 0; |
| } |
| #endif /* CONFIG_PM_DEVICE */ |
| |
| static const struct rtc_driver_api pcf8523_driver_api = { |
| .set_time = pcf8523_set_time, |
| .get_time = pcf8523_get_time, |
| #ifdef CONFIG_RTC_ALARM |
| .alarm_get_supported_fields = pcf8523_alarm_get_supported_fields, |
| .alarm_set_time = pcf8523_alarm_set_time, |
| .alarm_get_time = pcf8523_alarm_get_time, |
| .alarm_is_pending = pcf8523_alarm_is_pending, |
| #if PCF8523_INT1_GPIOS_IN_USE |
| .alarm_set_callback = pcf8523_alarm_set_callback, |
| #endif /* PCF8523_INT1_GPIOS_IN_USE */ |
| #endif /* CONFIG_RTC_ALARM */ |
| #if PCF8523_INT1_GPIOS_IN_USE && defined(CONFIG_RTC_UPDATE) |
| .update_set_callback = pcf8523_update_set_callback, |
| #endif /* PCF8523_INT1_GPIOS_IN_USE && defined(CONFIG_RTC_UPDATE) */ |
| #ifdef CONFIG_RTC_CALIBRATION |
| .set_calibration = pcf8523_set_calibration, |
| .get_calibration = pcf8523_get_calibration, |
| #endif /* CONFIG_RTC_CALIBRATION */ |
| }; |
| |
| #define PCF8523_PM_FROM_DT_INST(inst) \ |
| UTIL_CAT(PCF8523_PM_, DT_INST_STRING_UPPER_TOKEN(inst, battery_switch_over)) |
| #define PCF8523_CAP_SEL_FROM_DT_INST(inst) (DT_INST_PROP(inst, quartz_load_femtofarads) == 12500) |
| |
| #define PCF8523_INIT(inst) \ |
| static const struct pcf8523_config pcf8523_config_##inst = { \ |
| .i2c = I2C_DT_SPEC_INST_GET(inst), \ |
| .cof = DT_INST_ENUM_IDX(inst, clkout_frequency), \ |
| .pm = PCF8523_PM_FROM_DT_INST(inst), \ |
| .cap_sel = PCF8523_CAP_SEL_FROM_DT_INST(inst), \ |
| .wakeup_source = DT_INST_PROP(inst, wakeup_source), \ |
| IF_ENABLED(PCF8523_INT1_GPIOS_IN_USE, \ |
| (.int1 = GPIO_DT_SPEC_INST_GET_OR(inst, int1_gpios, {0})))}; \ |
| \ |
| static struct pcf8523_data pcf8523_data_##inst; \ |
| \ |
| PM_DEVICE_DT_INST_DEFINE(inst, pcf8523_pm_action); \ |
| \ |
| DEVICE_DT_INST_DEFINE(inst, &pcf8523_init, PM_DEVICE_DT_INST_GET(inst), \ |
| &pcf8523_data_##inst, &pcf8523_config_##inst, POST_KERNEL, \ |
| CONFIG_RTC_INIT_PRIORITY, &pcf8523_driver_api); |
| |
| DT_INST_FOREACH_STATUS_OKAY(PCF8523_INIT) |