| /* |
| * Copyright (c) 2024, Vitrolife A/S |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| * |
| * Datasheet: |
| * https://sensorsandpower.angst-pfister.com/fileadmin/products/datasheets/272/Manual-FCX-MLD_1620-21914-0033-E-0821.pdf |
| * |
| */ |
| |
| #define DT_DRV_COMPAT ap_fcx_mldx5 |
| #include <ctype.h> |
| #include <zephyr/kernel.h> |
| #include <zephyr/device.h> |
| #include <zephyr/drivers/sensor.h> |
| #include <zephyr/drivers/sensor/fcx_mldx5.h> |
| #include <zephyr/drivers/uart.h> |
| #include <zephyr/logging/log.h> |
| #include <zephyr/pm/device.h> |
| #include <zephyr/sys/util.h> |
| |
| LOG_MODULE_REGISTER(fcx_mldx5_sensor, CONFIG_SENSOR_LOG_LEVEL); |
| |
| #define FCX_MLDX5_STX 0x2 |
| #define FCX_MLDX5_ETX 0x3 |
| |
| #define FCX_MLDX5_STX_LEN 1 |
| #define FCX_MLDX5_CMD_LEN 2 |
| /* Data length depends on command type thus defined in array */ |
| #define FCX_MLDX5_CHECKSUM_LEN 2 |
| #define FCX_MLDX5_ETX_LEN 1 |
| #define FCX_MLDX5_HEADER_LEN \ |
| (FCX_MLDX5_STX_LEN + FCX_MLDX5_CMD_LEN + FCX_MLDX5_CHECKSUM_LEN + FCX_MLDX5_ETX_LEN) |
| |
| #define FCX_MLDX5_STX_INDEX 0 |
| #define FCX_MLDX5_CMD_INDEX (FCX_MLDX5_STX_INDEX + FCX_MLDX5_STX_LEN) |
| #define FCX_MLDX5_DATA_INDEX (FCX_MLDX5_CMD_INDEX + FCX_MLDX5_CMD_LEN) |
| #define FCX_MLDX5_CHECKSUM_INDEX(frame_len) ((frame_len)-FCX_MLDX5_CHECKSUM_LEN - FCX_MLDX5_ETX_LEN) |
| #define FCX_MLDX5_ETX_INDEX(frame_len) ((frame_len)-FCX_MLDX5_ETX_LEN) |
| |
| #define FCX_MLDX5_MAX_FRAME_LEN 11 |
| #define FCX_MLDX5_MAX_RESPONSE_DELAY 200 /* Not specified in datasheet */ |
| #define FCX_MLDX5_MAX_HEAT_UP_TIME 180000 |
| |
| struct fcx_mldx5_data { |
| struct k_mutex uart_mutex; |
| struct k_sem uart_rx_sem; |
| uint32_t o2_ppm; |
| uint8_t status; |
| uint8_t frame[FCX_MLDX5_MAX_FRAME_LEN]; |
| uint8_t frame_len; |
| }; |
| |
| struct fcx_mldx5_cfg { |
| const struct device *uart_dev; |
| uart_irq_callback_user_data_t cb; |
| }; |
| |
| enum fcx_mldx5_cmd { |
| FCX_MLDX5_CMD_READ_STATUS, |
| FCX_MLDX5_CMD_READ_O2_VALUE, |
| FCX_MLDX5_CMD_SWITCH_SENSOR_ON_OFF, |
| FCX_MLDX5_CMD_RESET, |
| FCX_MLDX5_CMD_ERROR, |
| }; |
| |
| enum fcx_mldx5_errors { |
| FCX_MLDX5_ERROR_CHECKSUM, |
| FCX_MLDX5_ERROR_UNKNOWN_COMMAND, |
| FCX_MLDX5_ERROR_PARAMETER, |
| FCX_MLDX5_ERROR_EEPROM, |
| }; |
| |
| static const char *const fcx_mldx5_cmds[] = { |
| [FCX_MLDX5_CMD_READ_STATUS] = "01", |
| [FCX_MLDX5_CMD_READ_O2_VALUE] = "02", |
| [FCX_MLDX5_CMD_SWITCH_SENSOR_ON_OFF] = "04", |
| [FCX_MLDX5_CMD_RESET] = "11", |
| [FCX_MLDX5_CMD_ERROR] = "EE", |
| }; |
| |
| static const uint8_t fcx_mldx5_cmds_data_len[] = { |
| [FCX_MLDX5_CMD_READ_STATUS] = 2, |
| [FCX_MLDX5_CMD_READ_O2_VALUE] = 5, |
| [FCX_MLDX5_CMD_SWITCH_SENSOR_ON_OFF] = 1, |
| [FCX_MLDX5_CMD_RESET] = 0, |
| [FCX_MLDX5_CMD_ERROR] = 2, |
| }; |
| |
| static const char *const fcx_mldx5_errors[] = { |
| [FCX_MLDX5_ERROR_CHECKSUM] = "checksum", |
| [FCX_MLDX5_ERROR_UNKNOWN_COMMAND] = "command", |
| [FCX_MLDX5_ERROR_PARAMETER] = "parameter", |
| [FCX_MLDX5_ERROR_EEPROM] = "eeprom", |
| }; |
| |
| static void fcx_mldx5_uart_flush(const struct device *uart_dev) |
| { |
| uint8_t tmp; |
| |
| while (uart_fifo_read(uart_dev, &tmp, 1) > 0) { |
| continue; |
| } |
| } |
| |
| static uint8_t fcx_mldx5_calculate_checksum(const uint8_t *buf, size_t len) |
| { |
| uint8_t checksum; |
| size_t i; |
| |
| if (buf == NULL || len == 0) { |
| return 0; |
| } |
| |
| checksum = buf[0]; |
| for (i = 1; i < len; ++i) { |
| checksum ^= buf[i]; |
| } |
| |
| return checksum; |
| } |
| |
| static int fcx_mldx5_frame_check_error(const struct fcx_mldx5_data *data, const char *command_sent) |
| { |
| const uint8_t len = FCX_MLDX5_HEADER_LEN + fcx_mldx5_cmds_data_len[FCX_MLDX5_CMD_ERROR]; |
| const char *command_error = fcx_mldx5_cmds[FCX_MLDX5_CMD_ERROR]; |
| const char *command_received = &data->frame[FCX_MLDX5_CMD_INDEX]; |
| const char *data_received = &data->frame[FCX_MLDX5_DATA_INDEX]; |
| uint8_t error; |
| |
| if (data->frame_len != len || |
| strncmp(command_error, command_received, FCX_MLDX5_CMD_LEN) != 0) { |
| return 0; |
| } |
| |
| if (data_received[0] != 'E' || char2hex(data_received[1], &error) != 0 || |
| error >= ARRAY_SIZE(fcx_mldx5_errors)) { |
| LOG_ERR("Could not parse error value %.*s", |
| fcx_mldx5_cmds_data_len[FCX_MLDX5_CMD_ERROR], data_received); |
| } else { |
| LOG_ERR("Command '%s' received error '%s'", command_sent, fcx_mldx5_errors[error]); |
| } |
| |
| return -EIO; |
| } |
| |
| static int fcx_mldx5_frame_verify(const struct fcx_mldx5_data *data, enum fcx_mldx5_cmd cmd) |
| { |
| const uint8_t frame_len = FCX_MLDX5_HEADER_LEN + fcx_mldx5_cmds_data_len[cmd]; |
| const char *command = fcx_mldx5_cmds[cmd]; |
| const char *command_received = &data->frame[FCX_MLDX5_CMD_INDEX]; |
| uint8_t checksum; |
| uint8_t checksum_received; |
| |
| if (fcx_mldx5_frame_check_error(data, command) != 0) { |
| return -EIO; |
| } else if (data->frame_len != frame_len) { |
| LOG_ERR("Expected command %s frame length %u not %u", command, frame_len, |
| data->frame_len); |
| return -EIO; |
| } else if (data->frame[FCX_MLDX5_STX_INDEX] != FCX_MLDX5_STX) { |
| LOG_ERR("No STX"); |
| return -EIO; |
| } else if (strncmp(command, command_received, FCX_MLDX5_CMD_LEN) != 0) { |
| LOG_ERR("Expected command %s not %.*s", command, FCX_MLDX5_CMD_LEN, |
| command_received); |
| return -EIO; |
| } else if (data->frame[FCX_MLDX5_ETX_INDEX(data->frame_len)] != FCX_MLDX5_ETX) { |
| LOG_ERR("No ETX"); |
| return -EIO; |
| } |
| |
| /* cmd and data bytes are used to calculate checksum */ |
| checksum = fcx_mldx5_calculate_checksum(command_received, |
| FCX_MLDX5_CMD_LEN + fcx_mldx5_cmds_data_len[cmd]); |
| checksum_received = |
| strtol(&data->frame[FCX_MLDX5_CHECKSUM_INDEX(data->frame_len)], NULL, 16); |
| if (checksum != checksum_received) { |
| LOG_ERR("Expected checksum 0x%02x not 0x%02x", checksum, checksum_received); |
| return -EIO; |
| } |
| |
| return 0; |
| } |
| |
| static void fcx_mldx5_uart_isr(const struct device *uart_dev, void *user_data) |
| { |
| const struct device *dev = user_data; |
| struct fcx_mldx5_data *data = dev->data; |
| int rc, read_len; |
| |
| if (!device_is_ready(uart_dev)) { |
| LOG_DBG("UART device is not ready"); |
| return; |
| } |
| |
| if (!uart_irq_update(uart_dev)) { |
| LOG_DBG("Unable to process interrupts"); |
| return; |
| } |
| |
| if (!uart_irq_rx_ready(uart_dev)) { |
| LOG_DBG("No RX data"); |
| return; |
| } |
| |
| read_len = FCX_MLDX5_MAX_FRAME_LEN - data->frame_len; |
| rc = read_len > 0 ? uart_fifo_read(uart_dev, &data->frame[data->frame_len], read_len) |
| : -ENOMEM; |
| |
| if (rc < 0) { |
| LOG_ERR("UART read failed: %d", rc < 0 ? rc : -ERANGE); |
| fcx_mldx5_uart_flush(uart_dev); |
| LOG_HEXDUMP_ERR(data->frame, data->frame_len, "Discarding"); |
| } else { |
| data->frame_len += rc; |
| if (data->frame[FCX_MLDX5_ETX_INDEX(data->frame_len)] != FCX_MLDX5_ETX) { |
| return; |
| } |
| LOG_HEXDUMP_DBG(data->frame, data->frame_len, "Frame received"); |
| } |
| |
| k_sem_give(&data->uart_rx_sem); |
| } |
| |
| static void fcx_mldx5_uart_send(const struct device *dev, enum fcx_mldx5_cmd cmd, |
| const char *cmd_data) |
| { |
| const struct fcx_mldx5_cfg *cfg = dev->config; |
| size_t cmd_data_len = cmd_data != NULL ? strlen(cmd_data) : 0; |
| size_t frame_len = FCX_MLDX5_HEADER_LEN + cmd_data_len; |
| char buf[FCX_MLDX5_MAX_FRAME_LEN]; |
| uint8_t checksum; |
| size_t i; |
| |
| buf[FCX_MLDX5_STX_INDEX] = FCX_MLDX5_STX; |
| memcpy(&buf[FCX_MLDX5_CMD_INDEX], fcx_mldx5_cmds[cmd], FCX_MLDX5_CMD_LEN); |
| if (cmd_data_len != 0) { |
| memcpy(&buf[FCX_MLDX5_DATA_INDEX], cmd_data, strlen(cmd_data)); |
| } |
| checksum = fcx_mldx5_calculate_checksum(&buf[FCX_MLDX5_CMD_INDEX], |
| FCX_MLDX5_CMD_LEN + cmd_data_len); |
| bin2hex(&checksum, 1, &buf[FCX_MLDX5_CHECKSUM_INDEX(frame_len)], |
| FCX_MLDX5_MAX_FRAME_LEN - FCX_MLDX5_CHECKSUM_INDEX(frame_len)); |
| buf[FCX_MLDX5_ETX_INDEX(frame_len)] = FCX_MLDX5_ETX; |
| |
| for (i = 0; i < frame_len; ++i) { |
| uart_poll_out(cfg->uart_dev, buf[i]); |
| } |
| |
| LOG_HEXDUMP_DBG(buf, frame_len, "Frame sent"); |
| } |
| |
| static int fcx_mldx5_await_receive(const struct device *dev) |
| { |
| int rc; |
| const struct fcx_mldx5_cfg *cfg = dev->config; |
| struct fcx_mldx5_data *data = dev->data; |
| |
| uart_irq_rx_enable(cfg->uart_dev); |
| |
| rc = k_sem_take(&data->uart_rx_sem, K_MSEC(FCX_MLDX5_MAX_RESPONSE_DELAY)); |
| |
| /* Reset semaphore if sensor did not respond within maximum specified response time |
| */ |
| if (rc == -EAGAIN) { |
| k_sem_reset(&data->uart_rx_sem); |
| } |
| |
| uart_irq_rx_disable(cfg->uart_dev); |
| |
| return rc; |
| } |
| |
| static int fcx_mldx5_read_status_value(struct fcx_mldx5_data *data, uint8_t data_len) |
| { |
| char *cmd_data_received = &data->frame[FCX_MLDX5_DATA_INDEX]; |
| uint8_t value; |
| |
| if (cmd_data_received[0] != '0' || char2hex(cmd_data_received[1], &value)) { |
| LOG_ERR("Could not parse status value %.*s", data_len, cmd_data_received); |
| return -EIO; |
| } |
| |
| switch (value) { |
| case FCX_MLDX5_STATUS_STANDBY: |
| break; |
| case FCX_MLDX5_STATUS_RAMP_UP: |
| break; |
| case FCX_MLDX5_STATUS_RUN: |
| break; |
| case FCX_MLDX5_STATUS_ERROR: |
| break; |
| default: |
| LOG_ERR("Status value %u invalid", value); |
| return -EIO; |
| } |
| |
| data->status = value; |
| return 0; |
| } |
| |
| static int fcx_mldx5_read_o2_value(struct fcx_mldx5_data *data) |
| { |
| const char *o2_data = &data->frame[FCX_MLDX5_DATA_INDEX]; |
| uint8_t o2_data_len = fcx_mldx5_cmds_data_len[FCX_MLDX5_CMD_READ_O2_VALUE]; |
| uint32_t value = 0; |
| size_t i; |
| |
| for (i = 0; i < o2_data_len; ++i) { |
| if (i == 2) { |
| if (o2_data[i] != '.') { |
| goto invalid_data; |
| } |
| } else if (isdigit((int)o2_data[i]) == 0) { |
| goto invalid_data; |
| } else { |
| value = value * 10 + (o2_data[i] - '0'); |
| } |
| } |
| |
| data->o2_ppm = value * 100; |
| return 0; |
| |
| invalid_data: |
| LOG_HEXDUMP_ERR(o2_data, o2_data_len, "Invalid O2 data"); |
| return -EIO; |
| } |
| |
| static int fcx_mldx5_buffer_process(struct fcx_mldx5_data *data, enum fcx_mldx5_cmd cmd, |
| const char *cmd_data) |
| { |
| if (fcx_mldx5_frame_verify(data, cmd) != 0) { |
| return -EIO; |
| } |
| |
| switch (cmd) { |
| case FCX_MLDX5_CMD_READ_STATUS: |
| return fcx_mldx5_read_status_value(data, fcx_mldx5_cmds_data_len[cmd]); |
| case FCX_MLDX5_CMD_READ_O2_VALUE: |
| return fcx_mldx5_read_o2_value(data); |
| case FCX_MLDX5_CMD_SWITCH_SENSOR_ON_OFF: |
| return cmd_data != NULL && data->frame[FCX_MLDX5_DATA_INDEX] == cmd_data[0]; |
| case FCX_MLDX5_CMD_RESET: |
| return 0; |
| default: |
| LOG_ERR("Unknown command 0x%02x", cmd); |
| return -EIO; |
| } |
| } |
| |
| static int fcx_mldx5_uart_transceive(const struct device *dev, enum fcx_mldx5_cmd cmd, |
| const char *cmd_data) |
| { |
| struct fcx_mldx5_data *data = dev->data; |
| int rc; |
| |
| k_mutex_lock(&data->uart_mutex, K_FOREVER); |
| |
| data->frame_len = 0; |
| fcx_mldx5_uart_send(dev, cmd, cmd_data); |
| |
| rc = fcx_mldx5_await_receive(dev); |
| if (rc != 0) { |
| LOG_ERR("%s did not receive a response: %d", fcx_mldx5_cmds[cmd], rc); |
| } else { |
| rc = fcx_mldx5_buffer_process(data, cmd, cmd_data); |
| } |
| |
| k_mutex_unlock(&data->uart_mutex); |
| |
| return rc; |
| } |
| |
| static int fcx_mldx5_attr_get(const struct device *dev, enum sensor_channel chan, |
| enum sensor_attribute attr, struct sensor_value *val) |
| { |
| struct fcx_mldx5_data *data = dev->data; |
| int rc; |
| |
| if (chan != SENSOR_CHAN_O2) { |
| return -ENOTSUP; |
| } |
| |
| switch (attr) { |
| case SENSOR_ATTR_FCX_MLDX5_STATUS: |
| rc = fcx_mldx5_uart_transceive(dev, FCX_MLDX5_CMD_READ_STATUS, NULL); |
| val->val1 = data->status; |
| return rc; |
| default: |
| return -ENOTSUP; |
| } |
| } |
| |
| static int fcx_mldx5_sample_fetch(const struct device *dev, enum sensor_channel chan) |
| { |
| if (chan != SENSOR_CHAN_O2 && chan != SENSOR_CHAN_ALL) { |
| return -ENOTSUP; |
| } |
| |
| return fcx_mldx5_uart_transceive(dev, FCX_MLDX5_CMD_READ_O2_VALUE, NULL); |
| } |
| |
| static int fcx_mldx5_channel_get(const struct device *dev, enum sensor_channel chan, |
| struct sensor_value *val) |
| { |
| struct fcx_mldx5_data *data = dev->data; |
| |
| if (chan != SENSOR_CHAN_O2) { |
| return -ENOTSUP; |
| } |
| |
| val->val1 = data->o2_ppm; |
| val->val2 = 0; |
| |
| return 0; |
| } |
| |
| static const struct sensor_driver_api fcx_mldx5_api_funcs = { |
| .attr_get = fcx_mldx5_attr_get, |
| .sample_fetch = fcx_mldx5_sample_fetch, |
| .channel_get = fcx_mldx5_channel_get, |
| }; |
| |
| #ifdef CONFIG_PM_DEVICE |
| static int pm_action(const struct device *dev, enum pm_device_action action) |
| { |
| switch (action) { |
| case PM_DEVICE_ACTION_RESUME: |
| return fcx_mldx5_uart_transceive(dev, FCX_MLDX5_CMD_SWITCH_SENSOR_ON_OFF, "1"); |
| case PM_DEVICE_ACTION_SUSPEND: |
| /* Standby with 20 % heating output */ |
| return fcx_mldx5_uart_transceive(dev, FCX_MLDX5_CMD_SWITCH_SENSOR_ON_OFF, "0"); |
| default: |
| return -ENOTSUP; |
| } |
| } |
| #endif |
| |
| static int fcx_mldx5_init(const struct device *dev) |
| { |
| int rc; |
| const struct fcx_mldx5_cfg *cfg = dev->config; |
| struct fcx_mldx5_data *data = dev->data; |
| |
| LOG_DBG("Initializing %s", dev->name); |
| |
| if (!device_is_ready(cfg->uart_dev)) { |
| return -ENODEV; |
| } |
| |
| k_mutex_init(&data->uart_mutex); |
| k_sem_init(&data->uart_rx_sem, 0, 1); |
| |
| uart_irq_rx_disable(cfg->uart_dev); |
| uart_irq_tx_disable(cfg->uart_dev); |
| |
| rc = uart_irq_callback_user_data_set(cfg->uart_dev, cfg->cb, (void *)dev); |
| if (rc != 0) { |
| LOG_ERR("UART IRQ setup failed: %d", rc); |
| return rc; |
| } |
| |
| /* Retry in case of garbled tx due to GPIO setup, crash during unfinished send or sensor |
| * start up time |
| */ |
| if (!WAIT_FOR(fcx_mldx5_uart_transceive(dev, FCX_MLDX5_CMD_READ_STATUS, NULL) == 0, |
| 1000 * USEC_PER_MSEC, k_msleep(10))) { |
| LOG_ERR("Read status failed"); |
| return -EIO; |
| } |
| |
| LOG_INF("%s status 0x%x", dev->name, data->status); |
| |
| return 0; |
| } |
| |
| #define FCX_MLDX5_INIT(n) \ |
| \ |
| static struct fcx_mldx5_data fcx_mldx5_data_##n = { \ |
| .status = FCX_MLDX5_STATUS_UNKNOWN, \ |
| }; \ |
| \ |
| static const struct fcx_mldx5_cfg fcx_mldx5_cfg_##n = { \ |
| .uart_dev = DEVICE_DT_GET(DT_INST_BUS(n)), \ |
| .cb = fcx_mldx5_uart_isr, \ |
| }; \ |
| \ |
| PM_DEVICE_DT_INST_DEFINE(n, pm_action); \ |
| \ |
| SENSOR_DEVICE_DT_INST_DEFINE(n, fcx_mldx5_init, PM_DEVICE_DT_INST_GET(n), \ |
| &fcx_mldx5_data_##n, &fcx_mldx5_cfg_##n, POST_KERNEL, \ |
| CONFIG_SENSOR_INIT_PRIORITY, &fcx_mldx5_api_funcs); |
| |
| DT_INST_FOREACH_STATUS_OKAY(FCX_MLDX5_INIT) |