/* * Copyright (c) 2019-2020 Peter Bigot Consulting, LLC * Copyright (c) 2021 Laird Connectivity * * SPDX-License-Identifier: Apache-2.0 */ #define DT_DRV_COMPAT microchip_mcp7940n #include #include #include #include #include #include #include #include #include #include LOG_MODULE_REGISTER(MCP7940N, CONFIG_COUNTER_LOG_LEVEL); /* Alarm channels */ #define ALARM0_ID 0 #define ALARM1_ID 1 /* Size of block when writing whole struct */ #define RTC_TIME_REGISTERS_SIZE sizeof(struct mcp7940n_time_registers) #define RTC_ALARM_REGISTERS_SIZE sizeof(struct mcp7940n_alarm_registers) /* Largest block size */ #define MAX_WRITE_SIZE (RTC_TIME_REGISTERS_SIZE) /* tm struct uses years since 1900 but unix time uses years since * 1970. MCP7940N default year is '1' so the offset is 69 */ #define UNIX_YEAR_OFFSET 69 /* Macro used to decode BCD to UNIX time to avoid potential copy and paste * errors. */ #define RTC_BCD_DECODE(reg_prefix) (reg_prefix##_one + reg_prefix##_ten * 10) struct mcp7940n_config { struct counter_config_info generic; struct i2c_dt_spec i2c; const struct gpio_dt_spec int_gpios; }; struct mcp7940n_data { const struct device *mcp7940n; struct k_sem lock; struct mcp7940n_time_registers registers; struct mcp7940n_alarm_registers alm0_registers; struct mcp7940n_alarm_registers alm1_registers; struct k_work alarm_work; struct gpio_callback int_callback; counter_alarm_callback_t counter_handler[2]; uint32_t counter_ticks[2]; void *alarm_user_data[2]; bool int_active_high; }; /** @brief Convert bcd time in device registers to UNIX time * * @param dev the MCP7940N device pointer. * * @retval returns unix time. */ static time_t decode_rtc(const struct device *dev) { struct mcp7940n_data *data = dev->data; time_t time_unix = 0; struct tm time = { 0 }; time.tm_sec = RTC_BCD_DECODE(data->registers.rtc_sec.sec); time.tm_min = RTC_BCD_DECODE(data->registers.rtc_min.min); time.tm_hour = RTC_BCD_DECODE(data->registers.rtc_hours.hr); time.tm_mday = RTC_BCD_DECODE(data->registers.rtc_date.date); time.tm_wday = data->registers.rtc_weekday.weekday; /* tm struct starts months at 0, mcp7940n starts at 1 */ time.tm_mon = RTC_BCD_DECODE(data->registers.rtc_month.month) - 1; /* tm struct uses years since 1900 but unix time uses years since 1970 */ time.tm_year = RTC_BCD_DECODE(data->registers.rtc_year.year) + UNIX_YEAR_OFFSET; time_unix = timeutil_timegm(&time); LOG_DBG("Unix time is %d\n", (uint32_t)time_unix); return time_unix; } /** @brief Encode time struct tm into mcp7940n rtc registers * * @param dev the MCP7940N device pointer. * @param time_buffer tm struct containing time to be encoded into mcp7940n * registers. * * @retval return 0 on success, or a negative error code from invalid * parameter. */ static int encode_rtc(const struct device *dev, struct tm *time_buffer) { struct mcp7940n_data *data = dev->data; uint8_t month; uint8_t year_since_epoch; /* In a tm struct, months start at 0, mcp7940n starts with 1 */ month = time_buffer->tm_mon + 1; if (time_buffer->tm_year < UNIX_YEAR_OFFSET) { return -EINVAL; } year_since_epoch = time_buffer->tm_year - UNIX_YEAR_OFFSET; /* Set external oscillator configuration bit */ data->registers.rtc_sec.start_osc = 1; data->registers.rtc_sec.sec_one = time_buffer->tm_sec % 10; data->registers.rtc_sec.sec_ten = time_buffer->tm_sec / 10; data->registers.rtc_min.min_one = time_buffer->tm_min % 10; data->registers.rtc_min.min_ten = time_buffer->tm_min / 10; data->registers.rtc_hours.hr_one = time_buffer->tm_hour % 10; data->registers.rtc_hours.hr_ten = time_buffer->tm_hour / 10; data->registers.rtc_weekday.weekday = time_buffer->tm_wday; data->registers.rtc_date.date_one = time_buffer->tm_mday % 10; data->registers.rtc_date.date_ten = time_buffer->tm_mday / 10; data->registers.rtc_month.month_one = month % 10; data->registers.rtc_month.month_ten = month / 10; data->registers.rtc_year.year_one = year_since_epoch % 10; data->registers.rtc_year.year_ten = year_since_epoch / 10; return 0; } /** @brief Encode time struct tm into mcp7940n alarm registers * * @param dev the MCP7940N device pointer. * @param time_buffer tm struct containing time to be encoded into mcp7940n * registers. * @param alarm_id alarm ID, can be 0 or 1 for MCP7940N. * * @retval return 0 on success, or a negative error code from invalid * parameter. */ static int encode_alarm(const struct device *dev, struct tm *time_buffer, uint8_t alarm_id) { struct mcp7940n_data *data = dev->data; uint8_t month; struct mcp7940n_alarm_registers *alm_regs; if (alarm_id == ALARM0_ID) { alm_regs = &data->alm0_registers; } else if (alarm_id == ALARM1_ID) { alm_regs = &data->alm1_registers; } else { return -EINVAL; } /* In a tm struct, months start at 0 */ month = time_buffer->tm_mon + 1; alm_regs->alm_sec.sec_one = time_buffer->tm_sec % 10; alm_regs->alm_sec.sec_ten = time_buffer->tm_sec / 10; alm_regs->alm_min.min_one = time_buffer->tm_min % 10; alm_regs->alm_min.min_ten = time_buffer->tm_min / 10; alm_regs->alm_hours.hr_one = time_buffer->tm_hour % 10; alm_regs->alm_hours.hr_ten = time_buffer->tm_hour / 10; alm_regs->alm_weekday.weekday = time_buffer->tm_wday; alm_regs->alm_date.date_one = time_buffer->tm_mday % 10; alm_regs->alm_date.date_ten = time_buffer->tm_mday / 10; alm_regs->alm_month.month_one = month % 10; alm_regs->alm_month.month_ten = month / 10; return 0; } /** @brief Reads single register from MCP7940N * * @param dev the MCP7940N device pointer. * @param addr register address. * @param val pointer to uint8_t that will contain register value if * successful. * * @retval return 0 on success, or a negative error code from an I2C * transaction. */ static int read_register(const struct device *dev, uint8_t addr, uint8_t *val) { const struct mcp7940n_config *cfg = dev->config; int rc = i2c_write_read_dt(&cfg->i2c, &addr, sizeof(addr), val, 1); return rc; } /** @brief Read registers from device and populate mcp7940n_registers struct * * @param dev the MCP7940N device pointer. * @param unix_time pointer to time_t value that will contain unix time if * successful. * * @retval return 0 on success, or a negative error code from an I2C * transaction. */ static int read_time(const struct device *dev, time_t *unix_time) { struct mcp7940n_data *data = dev->data; const struct mcp7940n_config *cfg = dev->config; uint8_t addr = REG_RTC_SEC; int rc = i2c_write_read_dt(&cfg->i2c, &addr, sizeof(addr), &data->registers, RTC_TIME_REGISTERS_SIZE); if (rc >= 0) { *unix_time = decode_rtc(dev); } return rc; } /** @brief Write a single register to MCP7940N * * @param dev the MCP7940N device pointer. * @param addr register address. * @param value Value that will be written to the register. * * @retval return 0 on success, or a negative error code from an I2C * transaction or invalid parameter. */ static int write_register(const struct device *dev, enum mcp7940n_register addr, uint8_t value) { const struct mcp7940n_config *cfg = dev->config; int rc = 0; uint8_t time_data[2] = {addr, value}; rc = i2c_write_dt(&cfg->i2c, time_data, sizeof(time_data)); return rc; } /** @brief Write a full time struct to MCP7940N registers. * * @param dev the MCP7940N device pointer. * @param addr first register address to write to, should be REG_RTC_SEC, * REG_ALM0_SEC or REG_ALM0_SEC. * @param size size of data struct that will be written. * * @retval return 0 on success, or a negative error code from an I2C * transaction or invalid parameter. */ static int write_data_block(const struct device *dev, enum mcp7940n_register addr, uint8_t size) { struct mcp7940n_data *data = dev->data; const struct mcp7940n_config *cfg = dev->config; int rc = 0; uint8_t time_data[MAX_WRITE_SIZE + 1]; uint8_t *write_block_start; if (size > MAX_WRITE_SIZE) { return -EINVAL; } if (addr >= REG_INVAL) { return -EINVAL; } if (addr == REG_RTC_SEC) { write_block_start = (uint8_t *)&data->registers; } else if (addr == REG_ALM0_SEC) { write_block_start = (uint8_t *)&data->alm0_registers; } else if (addr == REG_ALM1_SEC) { write_block_start = (uint8_t *)&data->alm1_registers; } else { return -EINVAL; } /* Load register address into first byte then fill in data values */ time_data[0] = addr; memcpy(&time_data[1], write_block_start, size); rc = i2c_write_dt(&cfg->i2c, time_data, size + 1); return rc; } /** @brief Sets the correct weekday. * * If the time is never set then the device defaults to 1st January 1970 * but with the wrong weekday set. This function ensures the weekday is * correct in this case. * * @param dev the MCP7940N device pointer. * @param unix_time pointer to unix time that will be used to work out the weekday * * @retval return 0 on success, or a negative error code from an I2C * transaction or invalid parameter. */ static int set_day_of_week(const struct device *dev, time_t *unix_time) { struct mcp7940n_data *data = dev->data; struct tm time_buffer = { 0 }; int rc = 0; if (gmtime_r(unix_time, &time_buffer) != NULL) { data->registers.rtc_weekday.weekday = time_buffer.tm_wday; rc = write_register(dev, REG_RTC_WDAY, *((uint8_t *)(&data->registers.rtc_weekday))); } else { rc = -EINVAL; } return rc; } /** @brief Checks the interrupt pending flag (IF) of a given alarm. * * A callback is fired if an IRQ is pending. * * @param dev the MCP7940N device pointer. * @param alarm_id ID of alarm, can be 0 or 1 for MCP7940N. */ static void mcp7940n_handle_interrupt(const struct device *dev, uint8_t alarm_id) { struct mcp7940n_data *data = dev->data; uint8_t alarm_reg_address; struct mcp7940n_alarm_registers *alm_regs; counter_alarm_callback_t cb; uint32_t ticks = 0; bool fire_callback = false; if (alarm_id == ALARM0_ID) { alarm_reg_address = REG_ALM0_WDAY; alm_regs = &data->alm0_registers; } else if (alarm_id == ALARM1_ID) { alarm_reg_address = REG_ALM1_WDAY; alm_regs = &data->alm1_registers; } else { return; } k_sem_take(&data->lock, K_FOREVER); /* Check if this alarm has a pending interrupt */ read_register(dev, alarm_reg_address, (uint8_t *)&alm_regs->alm_weekday); if (alm_regs->alm_weekday.alm_if) { /* Clear interrupt */ alm_regs->alm_weekday.alm_if = 0; write_register(dev, alarm_reg_address, *((uint8_t *)(&alm_regs->alm_weekday))); /* Fire callback */ if (data->counter_handler[alarm_id]) { cb = data->counter_handler[alarm_id]; ticks = data->counter_ticks[alarm_id]; fire_callback = true; } } k_sem_give(&data->lock); if (fire_callback) { cb(data->mcp7940n, 0, ticks, data->alarm_user_data[alarm_id]); } } static void mcp7940n_work_handler(struct k_work *work) { struct mcp7940n_data *data = CONTAINER_OF(work, struct mcp7940n_data, alarm_work); /* Check interrupt flags for both alarms */ mcp7940n_handle_interrupt(data->mcp7940n, ALARM0_ID); mcp7940n_handle_interrupt(data->mcp7940n, ALARM1_ID); } static void mcp7940n_init_cb(const struct device *dev, struct gpio_callback *gpio_cb, uint32_t pins) { struct mcp7940n_data *data = CONTAINER_OF(gpio_cb, struct mcp7940n_data, int_callback); ARG_UNUSED(pins); k_work_submit(&data->alarm_work); } int mcp7940n_rtc_set_time(const struct device *dev, time_t unix_time) { struct mcp7940n_data *data = dev->data; struct tm time_buffer = { 0 }; int rc = 0; if (unix_time > UINT32_MAX) { LOG_ERR("Unix time must be 32-bit"); return -EINVAL; } k_sem_take(&data->lock, K_FOREVER); /* Convert unix_time to civil time */ gmtime_r(&unix_time, &time_buffer); LOG_DBG("Desired time is %d-%d-%d %d:%d:%d\n", (time_buffer.tm_year + 1900), (time_buffer.tm_mon + 1), time_buffer.tm_mday, time_buffer.tm_hour, time_buffer.tm_min, time_buffer.tm_sec); /* Encode time */ rc = encode_rtc(dev, &time_buffer); if (rc < 0) { goto out; } /* Write to device */ rc = write_data_block(dev, REG_RTC_SEC, RTC_TIME_REGISTERS_SIZE); out: k_sem_give(&data->lock); return rc; } static int mcp7940n_counter_start(const struct device *dev) { struct mcp7940n_data *data = dev->data; int rc = 0; k_sem_take(&data->lock, K_FOREVER); /* Set start oscillator configuration bit */ data->registers.rtc_sec.start_osc = 1; rc = write_register(dev, REG_RTC_SEC, *((uint8_t *)(&data->registers.rtc_sec))); k_sem_give(&data->lock); return rc; } static int mcp7940n_counter_stop(const struct device *dev) { struct mcp7940n_data *data = dev->data; int rc = 0; k_sem_take(&data->lock, K_FOREVER); /* Clear start oscillator configuration bit */ data->registers.rtc_sec.start_osc = 0; rc = write_register(dev, REG_RTC_SEC, *((uint8_t *)(&data->registers.rtc_sec))); k_sem_give(&data->lock); return rc; } static int mcp7940n_counter_get_value(const struct device *dev, uint32_t *ticks) { struct mcp7940n_data *data = dev->data; time_t unix_time; int rc; k_sem_take(&data->lock, K_FOREVER); /* Get time */ rc = read_time(dev, &unix_time); /* Convert time to ticks */ if (rc >= 0) { *ticks = unix_time; } k_sem_give(&data->lock); return rc; } static int mcp7940n_counter_set_alarm(const struct device *dev, uint8_t alarm_id, const struct counter_alarm_cfg *alarm_cfg) { struct mcp7940n_data *data = dev->data; uint32_t seconds_until_alarm; time_t current_time; time_t alarm_time; struct tm time_buffer = { 0 }; uint8_t alarm_base_address; struct mcp7940n_alarm_registers *alm_regs; int rc = 0; k_sem_take(&data->lock, K_FOREVER); if (alarm_id == ALARM0_ID) { alarm_base_address = REG_ALM0_SEC; alm_regs = &data->alm0_registers; } else if (alarm_id == ALARM1_ID) { alarm_base_address = REG_ALM1_SEC; alm_regs = &data->alm1_registers; } else { rc = -EINVAL; goto out; } /* Convert ticks to time */ seconds_until_alarm = alarm_cfg->ticks; /* Get current time and add alarm offset */ rc = read_time(dev, ¤t_time); if (rc < 0) { goto out; } alarm_time = current_time + seconds_until_alarm; gmtime_r(&alarm_time, &time_buffer); /* Set alarm trigger mask and alarm enable flag */ if (alarm_id == ALARM0_ID) { data->registers.rtc_control.alm0_en = 1; } else if (alarm_id == ALARM1_ID) { data->registers.rtc_control.alm1_en = 1; } /* Set alarm to match with second, minute, hour, day of week, day of * month and month */ alm_regs->alm_weekday.alm_msk = MCP7940N_ALARM_TRIGGER_ALL; /* Write time to alarm registers */ encode_alarm(dev, &time_buffer, alarm_id); rc = write_data_block(dev, alarm_base_address, RTC_ALARM_REGISTERS_SIZE); if (rc < 0) { goto out; } /* Enable alarm */ rc = write_register(dev, REG_RTC_CONTROL, *((uint8_t *)(&data->registers.rtc_control))); if (rc < 0) { goto out; } /* Config user data and callback */ data->counter_handler[alarm_id] = alarm_cfg->callback; data->counter_ticks[alarm_id] = current_time; data->alarm_user_data[alarm_id] = alarm_cfg->user_data; out: k_sem_give(&data->lock); return rc; } static int mcp7940n_counter_cancel_alarm(const struct device *dev, uint8_t alarm_id) { struct mcp7940n_data *data = dev->data; int rc = 0; k_sem_take(&data->lock, K_FOREVER); /* Clear alarm enable bit */ if (alarm_id == ALARM0_ID) { data->registers.rtc_control.alm0_en = 0; } else if (alarm_id == ALARM1_ID) { data->registers.rtc_control.alm1_en = 0; } else { rc = -EINVAL; goto out; } rc = write_register(dev, REG_RTC_CONTROL, *((uint8_t *)(&data->registers.rtc_control))); out: k_sem_give(&data->lock); return rc; } static int mcp7940n_counter_set_top_value(const struct device *dev, const struct counter_top_cfg *cfg) { return -ENOTSUP; } /* This function can be used to poll the alarm interrupt flags if the MCU is * not connected to the MC7940N MFP pin. It can also be used to check if an * alarm was triggered while the MCU was in reset. This function will clear * the interrupt flag * * Return bitmask of alarm interrupt flag (IF) where each IF is shifted by * the alarm ID. */ static uint32_t mcp7940n_counter_get_pending_int(const struct device *dev) { struct mcp7940n_data *data = dev->data; uint32_t interrupt_pending = 0; int rc; k_sem_take(&data->lock, K_FOREVER); /* Check interrupt flag for alarm 0 */ rc = read_register(dev, REG_ALM0_WDAY, (uint8_t *)&data->alm0_registers.alm_weekday); if (rc < 0) { goto out; } if (data->alm0_registers.alm_weekday.alm_if) { /* Clear interrupt */ data->alm0_registers.alm_weekday.alm_if = 0; rc = write_register(dev, REG_ALM0_WDAY, *((uint8_t *)(&data->alm0_registers.alm_weekday))); if (rc < 0) { goto out; } interrupt_pending |= (1 << ALARM0_ID); } /* Check interrupt flag for alarm 1 */ rc = read_register(dev, REG_ALM1_WDAY, (uint8_t *)&data->alm1_registers.alm_weekday); if (rc < 0) { goto out; } if (data->alm1_registers.alm_weekday.alm_if) { /* Clear interrupt */ data->alm1_registers.alm_weekday.alm_if = 0; rc = write_register(dev, REG_ALM1_WDAY, *((uint8_t *)(&data->alm1_registers.alm_weekday))); if (rc < 0) { goto out; } interrupt_pending |= (1 << ALARM1_ID); } out: k_sem_give(&data->lock); if (rc) { interrupt_pending = 0; } return (interrupt_pending); } static uint32_t mcp7940n_counter_get_top_value(const struct device *dev) { return UINT32_MAX; } static int mcp7940n_init(const struct device *dev) { struct mcp7940n_data *data = dev->data; const struct mcp7940n_config *cfg = dev->config; int rc = 0; time_t unix_time = 0; /* Initialize and take the lock */ k_sem_init(&data->lock, 0, 1); if (!device_is_ready(cfg->i2c.bus)) { LOG_ERR("I2C device %s is not ready", cfg->i2c.bus->name); rc = -ENODEV; goto out; } rc = read_time(dev, &unix_time); if (rc < 0) { goto out; } rc = set_day_of_week(dev, &unix_time); if (rc < 0) { goto out; } /* Set 24-hour time */ data->registers.rtc_hours.twelve_hr = false; rc = write_register(dev, REG_RTC_HOUR, *((uint8_t *)(&data->registers.rtc_hours))); if (rc < 0) { goto out; } /* Configure alarm interrupt gpio */ if (cfg->int_gpios.port != NULL) { if (!gpio_is_ready_dt(&cfg->int_gpios)) { LOG_ERR("Port device %s is not ready", cfg->int_gpios.port->name); rc = -ENODEV; goto out; } data->mcp7940n = dev; k_work_init(&data->alarm_work, mcp7940n_work_handler); gpio_pin_configure_dt(&cfg->int_gpios, GPIO_INPUT); gpio_pin_interrupt_configure_dt(&cfg->int_gpios, GPIO_INT_EDGE_TO_ACTIVE); gpio_init_callback(&data->int_callback, mcp7940n_init_cb, BIT(cfg->int_gpios.pin)); gpio_add_callback(cfg->int_gpios.port, &data->int_callback); /* Configure interrupt polarity */ if ((cfg->int_gpios.dt_flags & GPIO_ACTIVE_LOW) == GPIO_ACTIVE_LOW) { data->int_active_high = false; } else { data->int_active_high = true; } data->alm0_registers.alm_weekday.alm_pol = data->int_active_high; data->alm1_registers.alm_weekday.alm_pol = data->int_active_high; rc = write_register(dev, REG_ALM0_WDAY, *((uint8_t *)(&data->alm0_registers.alm_weekday))); rc = write_register(dev, REG_ALM1_WDAY, *((uint8_t *)(&data->alm1_registers.alm_weekday))); } out: k_sem_give(&data->lock); return rc; } static const struct counter_driver_api mcp7940n_api = { .start = mcp7940n_counter_start, .stop = mcp7940n_counter_stop, .get_value = mcp7940n_counter_get_value, .set_alarm = mcp7940n_counter_set_alarm, .cancel_alarm = mcp7940n_counter_cancel_alarm, .set_top_value = mcp7940n_counter_set_top_value, .get_pending_int = mcp7940n_counter_get_pending_int, .get_top_value = mcp7940n_counter_get_top_value, }; #define INST_DT_MCP7904N(index) \ \ static struct mcp7940n_data mcp7940n_data_##index; \ \ static const struct mcp7940n_config mcp7940n_config_##index = { \ .generic = { \ .max_top_value = UINT32_MAX, \ .freq = 1, \ .flags = COUNTER_CONFIG_INFO_COUNT_UP, \ .channels = 2, \ }, \ .i2c = I2C_DT_SPEC_INST_GET(index), \ .int_gpios = GPIO_DT_SPEC_INST_GET_OR(index, int_gpios, {0}), \ }; \ \ DEVICE_DT_INST_DEFINE(index, mcp7940n_init, NULL, \ &mcp7940n_data_##index, \ &mcp7940n_config_##index, \ POST_KERNEL, \ CONFIG_COUNTER_INIT_PRIORITY, \ &mcp7940n_api); DT_INST_FOREACH_STATUS_OKAY(INST_DT_MCP7904N);