| // SPDX-License-Identifier: GPL-2.0+ |
| // |
| // fs-amp-lib.c --- Common library for FourSemi Audio Amplifiers |
| // |
| // Copyright (C) 2016-2025 Shanghai FourSemi Semiconductor Co.,Ltd. |
| |
| #include <linux/crc16.h> |
| #include <linux/device.h> |
| #include <linux/firmware.h> |
| #include <linux/module.h> |
| #include <linux/slab.h> |
| |
| #include "fs-amp-lib.h" |
| |
| static int fs_get_scene_count(struct fs_amp_lib *amp_lib) |
| { |
| const struct fs_fwm_table *table; |
| int count; |
| |
| if (!amp_lib || !amp_lib->dev) |
| return -EINVAL; |
| |
| table = amp_lib->table[FS_INDEX_SCENE]; |
| if (!table) |
| return -EFAULT; |
| |
| count = table->size / sizeof(struct fs_scene_index); |
| if (count < 1 || count > FS_SCENE_COUNT_MAX) { |
| dev_err(amp_lib->dev, "Invalid scene count: %d\n", count); |
| return -ERANGE; |
| } |
| |
| return count; |
| } |
| |
| static void fs_get_fwm_string(struct fs_amp_lib *amp_lib, |
| int offset, const char **pstr) |
| { |
| const struct fs_fwm_table *table; |
| |
| if (!amp_lib || !amp_lib->dev || !pstr) |
| return; |
| |
| table = amp_lib->table[FS_INDEX_STRING]; |
| if (table && offset > 0 && offset < table->size + sizeof(*table)) |
| *pstr = (char *)table + offset; |
| else |
| *pstr = NULL; |
| } |
| |
| static void fs_get_scene_reg(struct fs_amp_lib *amp_lib, |
| int offset, struct fs_amp_scene *scene) |
| { |
| const struct fs_fwm_table *table; |
| |
| if (!amp_lib || !amp_lib->dev || !scene) |
| return; |
| |
| table = amp_lib->table[FS_INDEX_REG]; |
| if (table && offset > 0 && offset < table->size + sizeof(*table)) |
| scene->reg = (struct fs_reg_table *)((char *)table + offset); |
| else |
| scene->reg = NULL; |
| } |
| |
| static void fs_get_scene_model(struct fs_amp_lib *amp_lib, |
| int offset, struct fs_amp_scene *scene) |
| { |
| const struct fs_fwm_table *table; |
| const char *ptr; |
| |
| if (!amp_lib || !amp_lib->dev || !scene) |
| return; |
| |
| table = amp_lib->table[FS_INDEX_MODEL]; |
| ptr = (char *)table; |
| if (table && offset > 0 && offset < table->size + sizeof(*table)) |
| scene->model = (struct fs_file_table *)(ptr + offset); |
| else |
| scene->model = NULL; |
| } |
| |
| static void fs_get_scene_effect(struct fs_amp_lib *amp_lib, |
| int offset, struct fs_amp_scene *scene) |
| { |
| const struct fs_fwm_table *table; |
| const char *ptr; |
| |
| if (!amp_lib || !amp_lib->dev || !scene) |
| return; |
| |
| table = amp_lib->table[FS_INDEX_EFFECT]; |
| ptr = (char *)table; |
| if (table && offset > 0 && offset < table->size + sizeof(*table)) |
| scene->effect = (struct fs_file_table *)(ptr + offset); |
| else |
| scene->effect = NULL; |
| } |
| |
| static int fs_parse_scene_tables(struct fs_amp_lib *amp_lib) |
| { |
| const struct fs_scene_index *scene_index; |
| const struct fs_fwm_table *table; |
| struct fs_amp_scene *scene; |
| int idx, count; |
| |
| if (!amp_lib || !amp_lib->dev) |
| return -EINVAL; |
| |
| count = fs_get_scene_count(amp_lib); |
| if (count <= 0) |
| return -EFAULT; |
| |
| scene = devm_kcalloc(amp_lib->dev, count, sizeof(*scene), GFP_KERNEL); |
| if (!scene) |
| return -ENOMEM; |
| |
| amp_lib->scene_count = count; |
| amp_lib->scene = scene; |
| |
| table = amp_lib->table[FS_INDEX_SCENE]; |
| scene_index = (struct fs_scene_index *)table->buf; |
| |
| for (idx = 0; idx < count; idx++) { |
| fs_get_fwm_string(amp_lib, scene_index->name, &scene->name); |
| if (!scene->name) |
| scene->name = devm_kasprintf(amp_lib->dev, |
| GFP_KERNEL, "S%d", idx); |
| dev_dbg(amp_lib->dev, "scene.%d name: %s\n", idx, scene->name); |
| fs_get_scene_reg(amp_lib, scene_index->reg, scene); |
| fs_get_scene_model(amp_lib, scene_index->model, scene); |
| fs_get_scene_effect(amp_lib, scene_index->effect, scene); |
| scene++; |
| scene_index++; |
| } |
| |
| return 0; |
| } |
| |
| static int fs_parse_all_tables(struct fs_amp_lib *amp_lib) |
| { |
| const struct fs_fwm_table *table; |
| const struct fs_fwm_index *index; |
| const char *ptr; |
| int idx, count; |
| int ret; |
| |
| if (!amp_lib || !amp_lib->dev || !amp_lib->hdr) |
| return -EINVAL; |
| |
| /* Parse all fwm tables */ |
| table = (struct fs_fwm_table *)amp_lib->hdr->params; |
| index = (struct fs_fwm_index *)table->buf; |
| count = table->size / sizeof(*index); |
| |
| for (idx = 0; idx < count; idx++, index++) { |
| if (index->type >= FS_INDEX_MAX) |
| return -ERANGE; |
| ptr = (char *)table + (int)index->offset; |
| amp_lib->table[index->type] = (struct fs_fwm_table *)ptr; |
| } |
| |
| /* Parse all scene tables */ |
| ret = fs_parse_scene_tables(amp_lib); |
| if (ret) |
| dev_err(amp_lib->dev, "Failed to parse scene: %d\n", ret); |
| |
| return ret; |
| } |
| |
| static int fs_verify_firmware(struct fs_amp_lib *amp_lib) |
| { |
| const struct fs_fwm_header *hdr; |
| int crcsum; |
| |
| if (!amp_lib || !amp_lib->dev || !amp_lib->hdr) |
| return -EINVAL; |
| |
| hdr = amp_lib->hdr; |
| |
| /* Verify the crcsum code */ |
| crcsum = crc16(0x0000, (const char *)&hdr->crc_size, hdr->crc_size); |
| if (crcsum != hdr->crc16) { |
| dev_err(amp_lib->dev, "Failed to checksum: %x-%x\n", |
| crcsum, hdr->crc16); |
| return -EFAULT; |
| } |
| |
| /* Verify the devid(chip_type) */ |
| if (amp_lib->devid != LO_U16(hdr->chip_type)) { |
| dev_err(amp_lib->dev, "DEVID dismatch: %04X#%04X\n", |
| amp_lib->devid, hdr->chip_type); |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| static void fs_print_firmware_info(struct fs_amp_lib *amp_lib) |
| { |
| const struct fs_fwm_header *hdr; |
| const char *pro_name = NULL; |
| const char *dev_name = NULL; |
| |
| if (!amp_lib || !amp_lib->dev || !amp_lib->hdr) |
| return; |
| |
| hdr = amp_lib->hdr; |
| |
| fs_get_fwm_string(amp_lib, hdr->project, &pro_name); |
| fs_get_fwm_string(amp_lib, hdr->device, &dev_name); |
| |
| dev_info(amp_lib->dev, "Project: %s Device: %s\n", |
| pro_name ? pro_name : "null", |
| dev_name ? dev_name : "null"); |
| |
| dev_info(amp_lib->dev, "Date: %04d%02d%02d-%02d%02d\n", |
| hdr->date.year, hdr->date.month, hdr->date.day, |
| hdr->date.hour, hdr->date.minute); |
| } |
| |
| int fs_amp_load_firmware(struct fs_amp_lib *amp_lib, const char *name) |
| { |
| const struct firmware *cont; |
| struct fs_fwm_header *hdr; |
| int ret; |
| |
| if (!amp_lib || !amp_lib->dev || !name) |
| return -EINVAL; |
| |
| ret = request_firmware(&cont, name, amp_lib->dev); |
| if (ret) { |
| dev_err(amp_lib->dev, "Failed to request %s: %d\n", name, ret); |
| return ret; |
| } |
| |
| dev_info(amp_lib->dev, "Loading %s - size: %zu\n", name, cont->size); |
| |
| hdr = devm_kmemdup(amp_lib->dev, cont->data, cont->size, GFP_KERNEL); |
| release_firmware(cont); |
| if (!hdr) |
| return -ENOMEM; |
| |
| amp_lib->hdr = hdr; |
| ret = fs_verify_firmware(amp_lib); |
| if (ret) { |
| amp_lib->hdr = NULL; |
| return ret; |
| } |
| |
| ret = fs_parse_all_tables(amp_lib); |
| if (ret) { |
| amp_lib->hdr = NULL; |
| return ret; |
| } |
| |
| fs_print_firmware_info(amp_lib); |
| |
| return 0; |
| } |
| EXPORT_SYMBOL_GPL(fs_amp_load_firmware); |
| |
| MODULE_AUTHOR("Nick Li <nick.li@foursemi.com>"); |
| MODULE_DESCRIPTION("FourSemi audio amplifier library"); |
| MODULE_LICENSE("GPL"); |