| /* |
| * Copyright (c) 2023, Nordic Semiconductor ASA |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #define DT_DRV_COMPAT zephyr_retention |
| |
| #include <string.h> |
| #include <sys/types.h> |
| #include <zephyr/kernel.h> |
| #include <zephyr/sys/util.h> |
| #include <zephyr/sys/crc.h> |
| #include <zephyr/device.h> |
| #include <zephyr/devicetree.h> |
| #include <zephyr/drivers/retained_mem.h> |
| #include <zephyr/retention/retention.h> |
| #include <zephyr/logging/log.h> |
| |
| LOG_MODULE_REGISTER(retention, CONFIG_RETENTION_LOG_LEVEL); |
| |
| #define DATA_VALID_VALUE 1 |
| |
| #define INST_HAS_CHECKSUM(n) DT_INST_PROP(n, checksum) || |
| |
| #define INST_HAS_PREFIX(n) COND_CODE_1(DT_INST_NODE_HAS_PROP(n, prefix), (1), (0)) || |
| |
| #if (DT_INST_FOREACH_STATUS_OKAY(INST_HAS_CHECKSUM) 0) |
| #define ANY_HAS_CHECKSUM |
| #endif |
| |
| #if (DT_INST_FOREACH_STATUS_OKAY(INST_HAS_PREFIX) 0) |
| #define ANY_HAS_PREFIX |
| #endif |
| |
| enum { |
| CHECKSUM_NONE = 0, |
| CHECKSUM_CRC8, |
| CHECKSUM_CRC16, |
| CHECKSUM_UNUSED, |
| CHECKSUM_CRC32, |
| }; |
| |
| struct retention_data { |
| bool header_written; |
| #ifdef CONFIG_RETENTION_MUTEXES |
| struct k_mutex lock; |
| #endif |
| }; |
| |
| struct retention_config { |
| const struct device *parent; |
| size_t offset; |
| size_t size; |
| size_t reserved_size; |
| uint8_t checksum_size; |
| uint8_t prefix_len; |
| uint8_t prefix[]; |
| }; |
| |
| static inline void retention_lock_take(const struct device *dev) |
| { |
| #ifdef CONFIG_RETENTION_MUTEXES |
| struct retention_data *data = dev->data; |
| |
| k_mutex_lock(&data->lock, K_FOREVER); |
| #else |
| ARG_UNUSED(dev); |
| #endif |
| } |
| |
| static inline void retention_lock_release(const struct device *dev) |
| { |
| #ifdef CONFIG_RETENTION_MUTEXES |
| struct retention_data *data = dev->data; |
| |
| k_mutex_unlock(&data->lock); |
| #else |
| ARG_UNUSED(dev); |
| #endif |
| } |
| |
| #ifdef ANY_HAS_CHECKSUM |
| static int retention_checksum(const struct device *dev, uint32_t *output) |
| { |
| const struct retention_config *config = dev->config; |
| int rc = -ENOSYS; |
| |
| if (config->checksum_size == CHECKSUM_CRC8 || |
| config->checksum_size == CHECKSUM_CRC16 || |
| config->checksum_size == CHECKSUM_CRC32) { |
| size_t pos = config->offset + config->prefix_len; |
| size_t end = config->offset + config->size - config->checksum_size; |
| uint8_t buffer[CONFIG_RETENTION_BUFFER_SIZE]; |
| |
| *output = 0; |
| |
| while (pos < end) { |
| uint8_t read_size = MIN((end - pos), sizeof(buffer)); |
| |
| rc = retained_mem_read(config->parent, pos, buffer, read_size); |
| |
| if (rc < 0) { |
| goto finish; |
| } |
| |
| if (config->checksum_size == CHECKSUM_CRC8) { |
| *output = (uint32_t)crc8(buffer, read_size, 0x12, |
| (uint8_t)*output, false); |
| } else if (config->checksum_size == CHECKSUM_CRC16) { |
| *output = (uint32_t)crc16_itu_t((uint16_t)*output, |
| buffer, read_size); |
| } else if (config->checksum_size == CHECKSUM_CRC32) { |
| *output = crc32_ieee_update(*output, buffer, read_size); |
| } |
| |
| pos += read_size; |
| } |
| } |
| |
| finish: |
| return rc; |
| } |
| #endif |
| |
| static int retention_init(const struct device *dev) |
| { |
| const struct retention_config *config = dev->config; |
| #ifdef CONFIG_RETENTION_MUTEXES |
| struct retention_data *data = dev->data; |
| #endif |
| ssize_t area_size; |
| |
| if (!device_is_ready(config->parent)) { |
| LOG_ERR("Parent device is not ready"); |
| return -ENODEV; |
| } |
| |
| /* Ensure backend has a large enough storage area for the requirements of |
| * this retention area |
| */ |
| area_size = retained_mem_size(config->parent); |
| |
| if (area_size < 0) { |
| LOG_ERR("Parent initialisation failure: %d", area_size); |
| return area_size; |
| } |
| |
| if ((config->offset + config->size) > area_size) { |
| /* Backend storage is insufficient */ |
| LOG_ERR("Underlying area size is insufficient, requires: 0x%x, has: 0x%x", |
| (config->offset + config->size), area_size); |
| return -EINVAL; |
| } |
| |
| #ifdef CONFIG_RETENTION_MUTEXES |
| k_mutex_init(&data->lock); |
| #endif |
| |
| return 0; |
| } |
| |
| ssize_t retention_size(const struct device *dev) |
| { |
| const struct retention_config *config = dev->config; |
| |
| return (config->size - config->reserved_size); |
| } |
| |
| int retention_is_valid(const struct device *dev) |
| { |
| const struct retention_config *config = dev->config; |
| int rc = 0; |
| |
| retention_lock_take(dev); |
| |
| /* If neither the header or checksum are enabled, return a not supported error */ |
| if (config->prefix_len == 0 && config->checksum_size == 0) { |
| rc = -ENOTSUP; |
| goto finish; |
| } |
| |
| #ifdef ANY_HAS_PREFIX |
| if (config->prefix_len != 0) { |
| /* Check magic header is present at the start of the section */ |
| struct retention_data *data = dev->data; |
| uint8_t buffer[CONFIG_RETENTION_BUFFER_SIZE]; |
| off_t pos = 0; |
| |
| while (pos < config->prefix_len) { |
| uint8_t read_size = MIN((config->prefix_len - pos), sizeof(buffer)); |
| |
| rc = retained_mem_read(config->parent, (config->offset + pos), buffer, |
| read_size); |
| |
| if (rc < 0) { |
| goto finish; |
| } |
| |
| if (memcmp(&config->prefix[pos], buffer, read_size) != 0) { |
| /* If the magic header does not match, do not check the rest of |
| * the validity of the data, assume it is invalid |
| */ |
| data->header_written = false; |
| rc = 0; |
| goto finish; |
| } |
| |
| pos += read_size; |
| } |
| |
| /* Header already exists so no need to re-write it again */ |
| data->header_written = true; |
| } |
| #endif |
| |
| #ifdef ANY_HAS_CHECKSUM |
| if (config->checksum_size != 0) { |
| /* Check the checksum validity, for this all the data must be read out */ |
| uint32_t checksum = 0; |
| uint32_t expected_checksum = 0; |
| ssize_t data_size = config->size - config->checksum_size; |
| |
| rc = retention_checksum(dev, &checksum); |
| |
| if (rc < 0) { |
| goto finish; |
| } |
| |
| if (config->checksum_size == CHECKSUM_CRC8) { |
| uint8_t read_checksum; |
| |
| rc = retained_mem_read(config->parent, (config->offset + data_size), |
| (void *)&read_checksum, sizeof(read_checksum)); |
| expected_checksum = (uint32_t)read_checksum; |
| } else if (config->checksum_size == CHECKSUM_CRC16) { |
| uint16_t read_checksum; |
| |
| rc = retained_mem_read(config->parent, (config->offset + data_size), |
| (void *)&read_checksum, sizeof(read_checksum)); |
| expected_checksum = (uint32_t)read_checksum; |
| } else if (config->checksum_size == CHECKSUM_CRC32) { |
| rc = retained_mem_read(config->parent, (config->offset + data_size), |
| (void *)&expected_checksum, |
| sizeof(expected_checksum)); |
| } |
| |
| if (rc < 0) { |
| goto finish; |
| } |
| |
| if (checksum != expected_checksum) { |
| goto finish; |
| } |
| } |
| #endif |
| |
| /* At this point, checks have passed (if enabled), mark data as being valid */ |
| rc = DATA_VALID_VALUE; |
| |
| finish: |
| retention_lock_release(dev); |
| |
| return rc; |
| } |
| |
| int retention_read(const struct device *dev, off_t offset, uint8_t *buffer, size_t size) |
| { |
| const struct retention_config *config = dev->config; |
| int rc; |
| |
| if (offset < 0 || ((size_t)offset + size) > (config->size - config->reserved_size)) { |
| /* Disallow reading past the virtual data size or before it */ |
| return -EINVAL; |
| } |
| |
| retention_lock_take(dev); |
| |
| rc = retained_mem_read(config->parent, (config->offset + config->prefix_len + |
| (size_t)offset), buffer, size); |
| |
| retention_lock_release(dev); |
| |
| return rc; |
| } |
| |
| int retention_write(const struct device *dev, off_t offset, const uint8_t *buffer, size_t size) |
| { |
| const struct retention_config *config = dev->config; |
| int rc; |
| |
| #ifdef ANY_HAS_PREFIX |
| struct retention_data *data = dev->data; |
| #endif |
| |
| retention_lock_take(dev); |
| |
| if (offset < 0 || ((size_t)offset + size) > (config->size - config->reserved_size)) { |
| /* Disallow writing past the virtual data size or before it */ |
| rc = -EINVAL; |
| goto finish; |
| } |
| |
| rc = retained_mem_write(config->parent, (config->offset + config->prefix_len + |
| (size_t)offset), buffer, size); |
| |
| if (rc < 0) { |
| goto finish; |
| } |
| |
| #ifdef ANY_HAS_PREFIX |
| /* Write optional header and footer information, these are done last to ensure data |
| * validity before marking it as being valid |
| */ |
| if (config->prefix_len != 0 && data->header_written == false) { |
| rc = retained_mem_write(config->parent, config->offset, (void *)config->prefix, |
| config->prefix_len); |
| |
| if (rc < 0) { |
| goto finish; |
| } |
| |
| data->header_written = true; |
| } |
| #endif |
| |
| #ifdef ANY_HAS_CHECKSUM |
| if (config->checksum_size != 0) { |
| /* Generating a checksum requires reading out all the data in the region */ |
| uint32_t checksum = 0; |
| |
| rc = retention_checksum(dev, &checksum); |
| |
| if (rc < 0) { |
| goto finish; |
| } |
| |
| if (config->checksum_size == CHECKSUM_CRC8) { |
| uint8_t output_checksum = (uint8_t)checksum; |
| |
| rc = retained_mem_write(config->parent, |
| (config->offset + config->size - config->checksum_size), |
| (void *)&output_checksum, sizeof(output_checksum)); |
| } else if (config->checksum_size == CHECKSUM_CRC16) { |
| uint16_t output_checksum = (uint16_t)checksum; |
| |
| rc = retained_mem_write(config->parent, |
| (config->offset + config->size - config->checksum_size), |
| (void *)&output_checksum, sizeof(output_checksum)); |
| } else if (config->checksum_size == CHECKSUM_CRC32) { |
| rc = retained_mem_write(config->parent, |
| (config->offset + config->size - config->checksum_size), |
| (void *)&checksum, sizeof(checksum)); |
| } |
| } |
| #endif |
| |
| finish: |
| retention_lock_release(dev); |
| |
| return rc; |
| } |
| |
| int retention_clear(const struct device *dev) |
| { |
| const struct retention_config *config = dev->config; |
| struct retention_data *data = dev->data; |
| int rc = 0; |
| uint8_t buffer[CONFIG_RETENTION_BUFFER_SIZE]; |
| off_t pos = 0; |
| |
| memset(buffer, 0, sizeof(buffer)); |
| |
| retention_lock_take(dev); |
| |
| data->header_written = false; |
| |
| while (pos < config->size) { |
| rc = retained_mem_write(config->parent, (config->offset + pos), buffer, |
| MIN((config->size - pos), sizeof(buffer))); |
| |
| if (rc < 0) { |
| goto finish; |
| } |
| |
| pos += MIN((config->size - pos), sizeof(buffer)); |
| } |
| |
| finish: |
| retention_lock_release(dev); |
| |
| return rc; |
| } |
| |
| static const struct retention_api retention_api = { |
| .size = retention_size, |
| .is_valid = retention_is_valid, |
| .read = retention_read, |
| .write = retention_write, |
| .clear = retention_clear, |
| }; |
| |
| #define RETENTION_DEVICE(inst) \ |
| static struct retention_data \ |
| retention_data_##inst = { \ |
| .header_written = false, \ |
| }; \ |
| static const struct retention_config \ |
| retention_config_##inst = { \ |
| .parent = DEVICE_DT_GET(DT_PARENT(DT_INST(inst, DT_DRV_COMPAT))), \ |
| .checksum_size = DT_INST_PROP(inst, checksum), \ |
| .offset = DT_INST_REG_ADDR(inst), \ |
| .size = DT_INST_REG_SIZE(inst), \ |
| .reserved_size = (COND_CODE_1(DT_INST_NODE_HAS_PROP(inst, prefix), \ |
| (DT_INST_PROP_LEN(inst, prefix)), (0)) + \ |
| DT_INST_PROP(inst, checksum)), \ |
| .prefix_len = COND_CODE_1(DT_INST_NODE_HAS_PROP(inst, prefix), \ |
| (DT_INST_PROP_LEN(inst, prefix)), (0)), \ |
| .prefix = DT_INST_PROP_OR(inst, prefix, {0}), \ |
| }; \ |
| DEVICE_DT_INST_DEFINE(inst, \ |
| &retention_init, \ |
| NULL, \ |
| &retention_data_##inst, \ |
| &retention_config_##inst, \ |
| POST_KERNEL, \ |
| CONFIG_RETENTION_INIT_PRIORITY, \ |
| &retention_api); |
| |
| DT_INST_FOREACH_STATUS_OKAY(RETENTION_DEVICE) |