| // SPDX-License-Identifier: GPL-2.0 | |
| #include <linux/array_size.h> | |
| #include <linux/debugfs.h> | |
| #include <linux/math64.h> | |
| #include <linux/mtd/spi-nor.h> | |
| #include <linux/spi/spi.h> | |
| #include <linux/spi/spi-mem.h> | |
| #include "core.h" | |
| #define SPI_NOR_DEBUGFS_ROOT "spi-nor" | |
| #define SNOR_F_NAME(name) [ilog2(SNOR_F_##name)] = #name | |
| static const char *const snor_f_names[] = { | |
| SNOR_F_NAME(HAS_SR_TB), | |
| SNOR_F_NAME(NO_OP_CHIP_ERASE), | |
| SNOR_F_NAME(BROKEN_RESET), | |
| SNOR_F_NAME(4B_OPCODES), | |
| SNOR_F_NAME(HAS_4BAIT), | |
| SNOR_F_NAME(HAS_LOCK), | |
| SNOR_F_NAME(HAS_16BIT_SR), | |
| SNOR_F_NAME(NO_READ_CR), | |
| SNOR_F_NAME(HAS_SR_TB_BIT6), | |
| SNOR_F_NAME(HAS_4BIT_BP), | |
| SNOR_F_NAME(HAS_SR_BP3_BIT6), | |
| SNOR_F_NAME(IO_MODE_EN_VOLATILE), | |
| SNOR_F_NAME(SOFT_RESET), | |
| SNOR_F_NAME(SWP_IS_VOLATILE), | |
| SNOR_F_NAME(RWW), | |
| SNOR_F_NAME(ECC), | |
| SNOR_F_NAME(NO_WP), | |
| SNOR_F_NAME(SWAP16), | |
| SNOR_F_NAME(HAS_SR2_CMP_BIT6), | |
| }; | |
| #undef SNOR_F_NAME | |
| static const char *spi_nor_protocol_name(enum spi_nor_protocol proto) | |
| { | |
| switch (proto) { | |
| case SNOR_PROTO_1_1_1: return "1S-1S-1S"; | |
| case SNOR_PROTO_1_1_2: return "1S-1S-2S"; | |
| case SNOR_PROTO_1_1_4: return "1S-1S-4S"; | |
| case SNOR_PROTO_1_1_8: return "1S-1S-8S"; | |
| case SNOR_PROTO_1_2_2: return "1S-2S-2S"; | |
| case SNOR_PROTO_1_4_4: return "1S-4S-4S"; | |
| case SNOR_PROTO_1_8_8: return "1S-8S-8S"; | |
| case SNOR_PROTO_2_2_2: return "2S-2S-2S"; | |
| case SNOR_PROTO_4_4_4: return "4S-4S-4S"; | |
| case SNOR_PROTO_8_8_8: return "8S-8S-8S"; | |
| case SNOR_PROTO_1_1_1_DTR: return "1D-1D-1D"; | |
| case SNOR_PROTO_1_2_2_DTR: return "1D-2D-2D"; | |
| case SNOR_PROTO_1_4_4_DTR: return "1D-4D-4D"; | |
| case SNOR_PROTO_1_8_8_DTR: return "1D-8D-8D"; | |
| case SNOR_PROTO_8_8_8_DTR: return "8D-8D-8D"; | |
| } | |
| return "<unknown>"; | |
| } | |
| static void spi_nor_print_flags(struct seq_file *s, unsigned long flags, | |
| const char *const *names, int names_len) | |
| { | |
| bool sep = false; | |
| int i; | |
| for (i = 0; i < sizeof(flags) * BITS_PER_BYTE; i++) { | |
| if (!(flags & BIT(i))) | |
| continue; | |
| if (sep) | |
| seq_puts(s, " | "); | |
| sep = true; | |
| if (i < names_len && names[i]) | |
| seq_puts(s, names[i]); | |
| else | |
| seq_printf(s, "1<<%d", i); | |
| } | |
| } | |
| static int spi_nor_params_show(struct seq_file *s, void *data) | |
| { | |
| struct spi_nor *nor = s->private; | |
| u64 min_prot_len = spi_nor_get_min_prot_length_sr(nor); | |
| struct spi_nor_flash_parameter *params = nor->params; | |
| struct spi_nor_erase_map *erase_map = ¶ms->erase_map; | |
| struct spi_nor_erase_region *region = erase_map->regions; | |
| const struct flash_info *info = nor->info; | |
| char buf[16], *str; | |
| loff_t lock_start; | |
| u64 lock_length; | |
| unsigned int i; | |
| seq_printf(s, "name\t\t%s\n", info->name); | |
| seq_printf(s, "id\t\t%*ph\n", SPI_NOR_MAX_ID_LEN, nor->id); | |
| string_get_size(params->size, 1, STRING_UNITS_2, buf, sizeof(buf)); | |
| seq_printf(s, "size\t\t%s\n", buf); | |
| seq_printf(s, "write size\t%u\n", params->writesize); | |
| seq_printf(s, "page size\t%u\n", params->page_size); | |
| seq_printf(s, "address nbytes\t%u\n", nor->addr_nbytes); | |
| seq_puts(s, "flags\t\t"); | |
| spi_nor_print_flags(s, nor->flags, snor_f_names, | |
| ARRAY_SIZE(snor_f_names)); | |
| seq_puts(s, "\n"); | |
| seq_puts(s, "\nopcodes\n"); | |
| seq_printf(s, " read\t\t0x%02x\n", nor->read_opcode); | |
| seq_printf(s, " dummy cycles\t%u\n", nor->read_dummy); | |
| seq_printf(s, " erase\t\t0x%02x\n", nor->erase_opcode); | |
| seq_printf(s, " program\t0x%02x\n", nor->program_opcode); | |
| switch (nor->cmd_ext_type) { | |
| case SPI_NOR_EXT_NONE: | |
| str = "none"; | |
| break; | |
| case SPI_NOR_EXT_REPEAT: | |
| str = "repeat"; | |
| break; | |
| case SPI_NOR_EXT_INVERT: | |
| str = "invert"; | |
| break; | |
| default: | |
| str = "<unknown>"; | |
| break; | |
| } | |
| seq_printf(s, " 8D extension\t%s\n", str); | |
| seq_puts(s, "\nprotocols\n"); | |
| seq_printf(s, " read\t\t%s\n", | |
| spi_nor_protocol_name(nor->read_proto)); | |
| seq_printf(s, " write\t\t%s\n", | |
| spi_nor_protocol_name(nor->write_proto)); | |
| seq_printf(s, " register\t%s\n", | |
| spi_nor_protocol_name(nor->reg_proto)); | |
| seq_puts(s, "\nerase commands\n"); | |
| for (i = 0; i < SNOR_ERASE_TYPE_MAX; i++) { | |
| struct spi_nor_erase_type *et = &erase_map->erase_type[i]; | |
| if (et->size) { | |
| string_get_size(et->size, 1, STRING_UNITS_2, buf, | |
| sizeof(buf)); | |
| seq_printf(s, " %02x (%s) [%d]\n", et->opcode, buf, i); | |
| } | |
| } | |
| if (!(nor->flags & SNOR_F_NO_OP_CHIP_ERASE)) { | |
| string_get_size(params->size, 1, STRING_UNITS_2, buf, sizeof(buf)); | |
| seq_printf(s, " %02x (%s)\n", params->die_erase_opcode, buf); | |
| } | |
| seq_puts(s, "\nsector map\n"); | |
| seq_puts(s, " region (in hex) | erase mask | overlaid\n"); | |
| seq_puts(s, " ------------------+------------+---------\n"); | |
| for (i = 0; i < erase_map->n_regions; i++) { | |
| u64 start = region[i].offset; | |
| u64 end = start + region[i].size - 1; | |
| u8 erase_mask = region[i].erase_mask; | |
| seq_printf(s, " %08llx-%08llx | [%c%c%c%c] | %s\n", | |
| start, end, | |
| erase_mask & BIT(0) ? '0' : ' ', | |
| erase_mask & BIT(1) ? '1' : ' ', | |
| erase_mask & BIT(2) ? '2' : ' ', | |
| erase_mask & BIT(3) ? '3' : ' ', | |
| region[i].overlaid ? "yes" : "no"); | |
| } | |
| if (!spi_nor_has_default_locking_ops(nor)) | |
| return 0; | |
| seq_puts(s, "\nlocked sectors\n"); | |
| seq_puts(s, " region (in hex) | status | #sectors\n"); | |
| seq_puts(s, " ------------------+----------+---------\n"); | |
| spi_nor_get_locked_range_sr(nor, nor->dfs_sr_cache, &lock_start, &lock_length); | |
| if (!lock_length || lock_length == params->size) { | |
| seq_printf(s, " %08llx-%08llx | %s | %llu\n", 0ULL, params->size - 1, | |
| lock_length ? " locked" : "unlocked", | |
| div_u64(params->size, min_prot_len)); | |
| } else if (!lock_start) { | |
| seq_printf(s, " %08llx-%08llx | %s | %llu\n", 0ULL, lock_length - 1, | |
| " locked", div_u64(lock_length, min_prot_len)); | |
| seq_printf(s, " %08llx-%08llx | %s | %llu\n", lock_length, params->size - 1, | |
| "unlocked", div_u64(params->size - lock_length, min_prot_len)); | |
| } else { | |
| seq_printf(s, " %08llx-%08llx | %s | %llu\n", 0ULL, lock_start - 1, | |
| "unlocked", div_u64(lock_start, min_prot_len)); | |
| seq_printf(s, " %08llx-%08llx | %s | %llu\n", lock_start, params->size - 1, | |
| " locked", div_u64(lock_length, min_prot_len)); | |
| } | |
| return 0; | |
| } | |
| DEFINE_SHOW_ATTRIBUTE(spi_nor_params); | |
| static int spi_nor_locked_sectors_map_show(struct seq_file *s, void *data) | |
| { | |
| struct spi_nor *nor = s->private; | |
| struct spi_nor_flash_parameter *params = nor->params; | |
| u64 min_prot_len = spi_nor_get_min_prot_length_sr(nor); | |
| unsigned int sector = 0; | |
| u64 offset = 0; | |
| bool locked; | |
| int i; | |
| seq_printf(s, "Locked sectors map (x: locked, .: unlocked, unit: %lldkiB)\n", | |
| min_prot_len / 1024); | |
| while (offset < params->size) { | |
| seq_printf(s, " 0x%08llx (#%5d): ", offset, sector); | |
| for (i = 0; i < 64 && offset < params->size; i++) { | |
| locked = spi_nor_is_locked_sr(nor, offset, min_prot_len, | |
| nor->dfs_sr_cache); | |
| if (locked) | |
| seq_puts(s, "x"); | |
| else | |
| seq_puts(s, "."); | |
| if (((i + 1) % 16) == 0) | |
| seq_puts(s, " "); | |
| offset += min_prot_len; | |
| sector++; | |
| } | |
| seq_puts(s, "\n"); | |
| } | |
| return 0; | |
| } | |
| DEFINE_SHOW_ATTRIBUTE(spi_nor_locked_sectors_map); | |
| static void spi_nor_print_read_cmd(struct seq_file *s, u32 cap, | |
| struct spi_nor_read_command *cmd) | |
| { | |
| seq_printf(s, " %s%s\n", spi_nor_protocol_name(cmd->proto), | |
| cap == SNOR_HWCAPS_READ_FAST ? " (fast read)" : ""); | |
| seq_printf(s, " opcode\t0x%02x\n", cmd->opcode); | |
| seq_printf(s, " mode cycles\t%u\n", cmd->num_mode_clocks); | |
| seq_printf(s, " dummy cycles\t%u\n", cmd->num_wait_states); | |
| } | |
| static void spi_nor_print_pp_cmd(struct seq_file *s, | |
| struct spi_nor_pp_command *cmd) | |
| { | |
| seq_printf(s, " %s\n", spi_nor_protocol_name(cmd->proto)); | |
| seq_printf(s, " opcode\t0x%02x\n", cmd->opcode); | |
| } | |
| static int spi_nor_capabilities_show(struct seq_file *s, void *data) | |
| { | |
| struct spi_nor *nor = s->private; | |
| struct spi_nor_flash_parameter *params = nor->params; | |
| u32 hwcaps = params->hwcaps.mask; | |
| int i, cmd; | |
| seq_puts(s, "Supported read modes by the flash\n"); | |
| for (i = 0; i < sizeof(hwcaps) * BITS_PER_BYTE; i++) { | |
| if (!(hwcaps & BIT(i))) | |
| continue; | |
| cmd = spi_nor_hwcaps_read2cmd(BIT(i)); | |
| if (cmd < 0) | |
| continue; | |
| spi_nor_print_read_cmd(s, BIT(i), ¶ms->reads[cmd]); | |
| hwcaps &= ~BIT(i); | |
| } | |
| seq_puts(s, "\nSupported page program modes by the flash\n"); | |
| for (i = 0; i < sizeof(hwcaps) * BITS_PER_BYTE; i++) { | |
| if (!(hwcaps & BIT(i))) | |
| continue; | |
| cmd = spi_nor_hwcaps_pp2cmd(BIT(i)); | |
| if (cmd < 0) | |
| continue; | |
| spi_nor_print_pp_cmd(s, ¶ms->page_programs[cmd]); | |
| hwcaps &= ~BIT(i); | |
| } | |
| if (hwcaps) | |
| seq_printf(s, "\nunknown hwcaps 0x%x\n", hwcaps); | |
| return 0; | |
| } | |
| DEFINE_SHOW_ATTRIBUTE(spi_nor_capabilities); | |
| static void spi_nor_debugfs_unregister(void *data) | |
| { | |
| struct spi_nor *nor = data; | |
| debugfs_remove(nor->debugfs_root); | |
| nor->debugfs_root = NULL; | |
| } | |
| static struct dentry *rootdir; | |
| void spi_nor_debugfs_register(struct spi_nor *nor) | |
| { | |
| struct dentry *d; | |
| int ret; | |
| if (!rootdir) | |
| rootdir = debugfs_create_dir(SPI_NOR_DEBUGFS_ROOT, NULL); | |
| ret = devm_add_action(nor->dev, spi_nor_debugfs_unregister, nor); | |
| if (ret) | |
| return; | |
| d = debugfs_create_dir(dev_name(nor->dev), rootdir); | |
| nor->debugfs_root = d; | |
| debugfs_create_file("params", 0444, d, nor, &spi_nor_params_fops); | |
| debugfs_create_file("capabilities", 0444, d, nor, | |
| &spi_nor_capabilities_fops); | |
| if (spi_nor_has_default_locking_ops(nor)) | |
| debugfs_create_file("locked-sectors-map", 0444, d, nor, | |
| &spi_nor_locked_sectors_map_fops); | |
| } | |
| void spi_nor_debugfs_shutdown(void) | |
| { | |
| debugfs_remove(rootdir); | |
| } |