/* Copyright (c) 2016-2018, The Linux Foundation. All rights reserved. * Copyright (c) 2022-2023, Qualcomm Innovation Center, Inc. All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 and * only version 2 as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ #include #include #include #include #if 1 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef CONFIG_CNSS2_IPC_LOGGING #include #endif #endif #include "main.h" #include "debug.h" #include "pci.h" #define CNSS_IPC_LOG_PAGES 32 #ifdef CONFIG_CNSS2_IPC_LOGGING void *cnss_ipc_log_context; void *cnss_ipc_log_long_context; EXPORT_SYMBOL(cnss_ipc_log_context); #endif extern void cnss_dump_qmi_history(void); struct dentry *cnss_root_dentry = NULL; int log_level = CNSS_LOG_LEVEL_INFO; EXPORT_SYMBOL(log_level); module_param(log_level, int, 0644); MODULE_PARM_DESC(log_level, "CNSS2 Module Log Level"); #ifdef CONFIG_CNSS2_IPC_LOGGING #define CONFIG_IPC_LOG_MINIDUMP_BUFFERS 0x00080000 #define LOG_PAGE_DATA_SIZE sizeof(((struct ipc_log_page *)0)->data) #define LOG_PAGE_FLAG (1 << 31) #define MAX_MINIDUMP_BUFFERS CONFIG_IPC_LOG_MINIDUMP_BUFFERS /*16th bit is used for minidump feature*/ #define FEATURE_MASK 0x10000 static int minidump_buf_cnt; static LIST_HEAD(ipc_log_context_list); static DEFINE_RWLOCK(context_list_lock_lha1); static void *get_deserialization_func(struct ipc_log_context *ilctxt, int type); static DEFINE_MUTEX(ipc_log_debugfs_init_lock); static struct dentry *root_dent; static int debug_log(struct ipc_log_context *ilctxt, char *buff, int size, int cont) { int i = 0; int ret; if (size < MAX_MSG_DECODED_SIZE) { pr_err("%s: buffer size %d < %d\n", __func__, size, MAX_MSG_DECODED_SIZE); return -ENOMEM; } do { i = ipc_log_extract(ilctxt, buff, size - 1); if (cont && i == 0) { ret = wait_for_completion_interruptible( &ilctxt->read_avail); if (ret < 0) return ret; } } while (cont && i == 0); return i; } /* * VFS Read operation helper which dispatches the call to the debugfs * read command stored in file->private_data. * * @file File structure * @buff user buffer * @count size of user buffer * @ppos file position to read from (only a value of 0 is accepted) * @cont 1 = continuous mode (don't return 0 to signal end-of-file) * * @returns ==0 end of file * >0 number of bytes read * <0 error */ static ssize_t debug_read_helper(struct file *file, char __user *buff, size_t count, loff_t *ppos, int cont) { struct ipc_log_context *ilctxt; struct dentry *d = file->f_path.dentry; char *buffer; int bsize; int r; r = debugfs_file_get(d); if (r) return r; ilctxt = file->private_data; r = kref_get_unless_zero(&ilctxt->refcount) ? 0 : -EIO; if (r) { debugfs_file_put(d); return r; } buffer = kmalloc(count, GFP_KERNEL); if (!buffer) { bsize = -ENOMEM; goto done; } bsize = debug_log(ilctxt, buffer, count, cont); if (bsize > 0) { if (copy_to_user(buff, buffer, bsize)) { bsize = -EFAULT; kfree(buffer); goto done; } *ppos += bsize; } kfree(buffer); done: ipc_log_context_put(ilctxt); debugfs_file_put(d); return bsize; } static ssize_t debug_read(struct file *file, char __user *buff, size_t count, loff_t *ppos) { return debug_read_helper(file, buff, count, ppos, 0); } static ssize_t debug_read_cont(struct file *file, char __user *buff, size_t count, loff_t *ppos) { return debug_read_helper(file, buff, count, ppos, 1); } static const struct file_operations debug_ops = { .read = debug_read, .open = simple_open, }; static const struct file_operations debug_ops_cont = { .read = debug_read_cont, .open = simple_open, }; static void debug_create(const char *name, mode_t mode, struct dentry *dent, struct ipc_log_context *ilctxt, const struct file_operations *fops) { debugfs_create_file_unsafe(name, mode, dent, ilctxt, fops); } static void dfunc_string(struct encode_context *ectxt, struct decode_context *dctxt) { tsv_timestamp_read(ectxt, dctxt, ""); tsv_qtimer_read(ectxt, dctxt, " "); tsv_byte_array_read(ectxt, dctxt, ""); /* add trailing \n if necessary */ if (*(dctxt->buff - 1) != '\n') { if (dctxt->size) { ++dctxt->buff; --dctxt->size; } *(dctxt->buff - 1) = '\n'; } } void check_and_create_debugfs(void) { mutex_lock(&ipc_log_debugfs_init_lock); if (!root_dent) { root_dent = debugfs_create_dir("cnss_ipc_logging", NULL); if (IS_ERR(root_dent)) { pr_err("%s: unable to create debugfs %ld\n", __func__, PTR_ERR(root_dent)); root_dent = NULL; } } mutex_unlock(&ipc_log_debugfs_init_lock); } EXPORT_SYMBOL(check_and_create_debugfs); void create_ctx_debugfs(struct ipc_log_context *ctxt, const char *mod_name) { if (!root_dent) check_and_create_debugfs(); if (root_dent) { ctxt->dent = debugfs_create_dir(mod_name, root_dent); if (!IS_ERR(ctxt->dent)) { debug_create("log", 0444, ctxt->dent, ctxt, &debug_ops); debug_create("log_cont", 0444, ctxt->dent, ctxt, &debug_ops_cont); } } add_deserialization_func((void *)ctxt, TSV_TYPE_STRING, dfunc_string); } EXPORT_SYMBOL(create_ctx_debugfs); static struct ipc_log_page *get_first_page(struct ipc_log_context *ilctxt) { struct ipc_log_page_header *p_pghdr; struct ipc_log_page *pg = NULL; if (!ilctxt) return NULL; p_pghdr = list_first_entry(&ilctxt->page_list, struct ipc_log_page_header, list); pg = container_of(p_pghdr, struct ipc_log_page, hdr); return pg; } /** * is_nd_read_empty - Returns true if no data is available to read in log * * @ilctxt: logging context * @returns: > 1 if context is empty; 0 if not empty; <0 for failure * * This is for the debugfs read pointer which allows for a non-destructive read. * There may still be data in the log, but it may have already been read. */ static int is_nd_read_empty(struct ipc_log_context *ilctxt) { if (!ilctxt) return -EINVAL; return ((ilctxt->nd_read_page == ilctxt->write_page) && (ilctxt->nd_read_page->hdr.nd_read_offset == ilctxt->write_page->hdr.write_offset)); } /** * is_read_empty - Returns true if no data is available in log * * @ilctxt: logging context * @returns: > 1 if context is empty; 0 if not empty; <0 for failure * * This is for the actual log contents. If it is empty, then there * is no data at all in the log. */ static int is_read_empty(struct ipc_log_context *ilctxt) { if (!ilctxt) return -EINVAL; return ((ilctxt->read_page == ilctxt->write_page) && (ilctxt->read_page->hdr.read_offset == ilctxt->write_page->hdr.write_offset)); } /** * is_nd_read_equal_read - Return true if the non-destructive read is equal to * the destructive read * * @ilctxt: logging context * @returns: true if nd read is equal to read; false otherwise */ static bool is_nd_read_equal_read(struct ipc_log_context *ilctxt) { uint16_t read_offset; uint16_t nd_read_offset; if (ilctxt->nd_read_page == ilctxt->read_page) { read_offset = ilctxt->read_page->hdr.read_offset; nd_read_offset = ilctxt->nd_read_page->hdr.nd_read_offset; if (read_offset == nd_read_offset) return true; } return false; } static struct ipc_log_page *get_next_page(struct ipc_log_context *ilctxt, struct ipc_log_page *cur_pg) { struct ipc_log_page_header *p_pghdr; struct ipc_log_page *pg = NULL; if (!ilctxt || !cur_pg) return NULL; if (ilctxt->last_page == cur_pg) return ilctxt->first_page; p_pghdr = list_first_entry(&cur_pg->hdr.list, struct ipc_log_page_header, list); pg = container_of(p_pghdr, struct ipc_log_page, hdr); return pg; } static void register_minidump(u64 vaddr, u64 size, const char *buf_name, int index) { struct md_region md_entry; int ret; if (msm_minidump_enabled() && (minidump_buf_cnt < MAX_MINIDUMP_BUFFERS)) { scnprintf(md_entry.name, sizeof(md_entry.name), "%s_%d", buf_name, index); md_entry.virt_addr = vaddr; md_entry.phys_addr = virt_to_phys((void *)vaddr); md_entry.size = size; ret = msm_minidump_add_region(&md_entry); if (ret < 0) { pr_err( "Failed to register log buffer %s_%d in Minidump ret %d\n", buf_name, index, ret); return; } minidump_buf_cnt++; } } /** * ipc_log_read - do non-destructive read of the log * * @ilctxt: Logging context * @data: Data pointer to receive the data * @data_size: Number of bytes to read (must be <= bytes available in log) * * This read will update a runtime read pointer, but will not affect the actual * contents of the log which allows for reading the logs continuously while * debugging and if the system crashes, then the full logs can still be * extracted. */ static void ipc_log_read(struct ipc_log_context *ilctxt, void *data, int data_size) { int bytes_to_read; bytes_to_read = MIN(LOG_PAGE_DATA_SIZE - ilctxt->nd_read_page->hdr.nd_read_offset, data_size); memcpy(data, (ilctxt->nd_read_page->data + ilctxt->nd_read_page->hdr.nd_read_offset), bytes_to_read); if (bytes_to_read != data_size) { /* not enough space, wrap read to next page */ ilctxt->nd_read_page->hdr.nd_read_offset = 0; ilctxt->nd_read_page = get_next_page(ilctxt, ilctxt->nd_read_page); if (WARN_ON(ilctxt->nd_read_page == NULL)) return; memcpy((data + bytes_to_read), (ilctxt->nd_read_page->data + ilctxt->nd_read_page->hdr.nd_read_offset), (data_size - bytes_to_read)); bytes_to_read = (data_size - bytes_to_read); } ilctxt->nd_read_page->hdr.nd_read_offset += bytes_to_read; } /** * ipc_log_drop - do destructive read of the log * * @ilctxt: Logging context * @data: Data pointer to receive the data (or NULL) * @data_size: Number of bytes to read (must be <= bytes available in log) */ static void ipc_log_drop(struct ipc_log_context *ilctxt, void *data, int data_size) { int bytes_to_read; bool push_nd_read; bytes_to_read = MIN(LOG_PAGE_DATA_SIZE - ilctxt->read_page->hdr.read_offset, data_size); if (data) memcpy(data, (ilctxt->read_page->data + ilctxt->read_page->hdr.read_offset), bytes_to_read); if (bytes_to_read != data_size) { /* not enough space, wrap read to next page */ push_nd_read = is_nd_read_equal_read(ilctxt); ilctxt->read_page->hdr.read_offset = 0; if (push_nd_read) { ilctxt->read_page->hdr.nd_read_offset = 0; ilctxt->read_page = get_next_page(ilctxt, ilctxt->read_page); if (WARN_ON(ilctxt->read_page == NULL)) return; ilctxt->nd_read_page = ilctxt->read_page; } else { ilctxt->read_page = get_next_page(ilctxt, ilctxt->read_page); if (WARN_ON(ilctxt->read_page == NULL)) return; } if (data) memcpy((data + bytes_to_read), (ilctxt->read_page->data + ilctxt->read_page->hdr.read_offset), (data_size - bytes_to_read)); bytes_to_read = (data_size - bytes_to_read); } /* update non-destructive read pointer if necessary */ push_nd_read = is_nd_read_equal_read(ilctxt); ilctxt->read_page->hdr.read_offset += bytes_to_read; ilctxt->write_avail += data_size; if (push_nd_read) ilctxt->nd_read_page->hdr.nd_read_offset += bytes_to_read; } /** * msg_read - Reads a message. * * If a message is read successfully, then the message context * will be set to: * .hdr message header .size and .type values * .offset beginning of message data * * @ilctxt Logging context * @ectxt Message context * * @returns 0 - no message available; >0 message size; <0 error */ static int msg_read(struct ipc_log_context *ilctxt, struct encode_context *ectxt) { struct tsv_header hdr; if (!ectxt) return -EINVAL; if (is_nd_read_empty(ilctxt)) return 0; ipc_log_read(ilctxt, &hdr, sizeof(hdr)); ectxt->hdr.type = hdr.type; ectxt->hdr.size = hdr.size; ectxt->offset = sizeof(hdr); ipc_log_read(ilctxt, (ectxt->buff + ectxt->offset), (int)hdr.size); return sizeof(hdr) + (int)hdr.size; } /** * msg_drop - Drops a message. * * @ilctxt Logging context */ static void msg_drop(struct ipc_log_context *ilctxt) { struct tsv_header hdr; if (!is_read_empty(ilctxt)) { ipc_log_drop(ilctxt, &hdr, sizeof(hdr)); ipc_log_drop(ilctxt, NULL, (int)hdr.size); } } /* * Commits messages to the FIFO. If the FIFO is full, then enough * messages are dropped to create space for the new message. */ void ipc_log_write(void *ctxt, struct encode_context *ectxt) { struct ipc_log_context *ilctxt = (struct ipc_log_context *)ctxt; int bytes_to_write; unsigned long flags; if (!ilctxt || !ectxt) { pr_err("%s: Invalid ipc_log or encode context\n", __func__); return; } read_lock_irqsave(&context_list_lock_lha1, flags); spin_lock(&ilctxt->context_lock_lhb1); while (ilctxt->write_avail <= ectxt->offset) msg_drop(ilctxt); bytes_to_write = MIN(LOG_PAGE_DATA_SIZE - ilctxt->write_page->hdr.write_offset, ectxt->offset); memcpy((ilctxt->write_page->data + ilctxt->write_page->hdr.write_offset), ectxt->buff, bytes_to_write); if (bytes_to_write != ectxt->offset) { uint64_t t_now = sched_clock(); ilctxt->write_page->hdr.write_offset += bytes_to_write; ilctxt->write_page->hdr.end_time = t_now; ilctxt->write_page = get_next_page(ilctxt, ilctxt->write_page); if (WARN_ON(ilctxt->write_page == NULL)) { spin_unlock(&ilctxt->context_lock_lhb1); read_unlock_irqrestore(&context_list_lock_lha1, flags); return; } ilctxt->write_page->hdr.write_offset = 0; ilctxt->write_page->hdr.start_time = t_now; memcpy((ilctxt->write_page->data + ilctxt->write_page->hdr.write_offset), (ectxt->buff + bytes_to_write), (ectxt->offset - bytes_to_write)); bytes_to_write = (ectxt->offset - bytes_to_write); } ilctxt->write_page->hdr.write_offset += bytes_to_write; ilctxt->write_avail -= ectxt->offset; complete(&ilctxt->read_avail); spin_unlock(&ilctxt->context_lock_lhb1); read_unlock_irqrestore(&context_list_lock_lha1, flags); } EXPORT_SYMBOL(ipc_log_write); /* * Starts a new message after which you can add serialized data and * then complete the message by calling msg_encode_end(). */ void msg_encode_start(struct encode_context *ectxt, uint32_t type) { if (!ectxt) { pr_err("%s: Invalid encode context\n", __func__); return; } ectxt->hdr.type = type; ectxt->hdr.size = 0; ectxt->offset = sizeof(ectxt->hdr); } EXPORT_SYMBOL(msg_encode_start); /* * Completes the message */ void msg_encode_end(struct encode_context *ectxt) { if (!ectxt) { pr_err("%s: Invalid encode context\n", __func__); return; } /* finalize data size */ ectxt->hdr.size = ectxt->offset - sizeof(ectxt->hdr); memcpy(ectxt->buff, &ectxt->hdr, sizeof(ectxt->hdr)); } EXPORT_SYMBOL(msg_encode_end); /* * Helper function used to write data to a message context. * * @ectxt context initialized by calling msg_encode_start() * @data data to write * @size number of bytes of data to write */ static inline int tsv_write_data(struct encode_context *ectxt, void *data, uint32_t size) { if (!ectxt) { pr_err("%s: Invalid encode context\n", __func__); return -EINVAL; } if ((ectxt->offset + size) > MAX_MSG_SIZE) { pr_err("%s: No space to encode further\n", __func__); return -EINVAL; } memcpy((void *)(ectxt->buff + ectxt->offset), data, size); ectxt->offset += size; return 0; } /* * Helper function that writes a type to the context. * * @ectxt context initialized by calling msg_encode_start() * @type primitive type * @size size of primitive in bytes */ static inline int tsv_write_header(struct encode_context *ectxt, uint32_t type, uint32_t size) { struct tsv_header hdr; hdr.type = (unsigned char)type; hdr.size = (unsigned char)size; return tsv_write_data(ectxt, &hdr, sizeof(hdr)); } /* * Writes the current timestamp count. * * @ectxt context initialized by calling msg_encode_start() */ int tsv_timestamp_write(struct encode_context *ectxt) { int ret; uint64_t t_now = sched_clock(); ret = tsv_write_header(ectxt, TSV_TYPE_TIMESTAMP, sizeof(t_now)); if (ret) return ret; return tsv_write_data(ectxt, &t_now, sizeof(t_now)); } EXPORT_SYMBOL(tsv_timestamp_write); /* * Writes the current QTimer timestamp count. * * @ectxt context initialized by calling msg_encode_start() */ int tsv_qtimer_write(struct encode_context *ectxt) { int ret; uint64_t t_now = arch_timer_read_counter(); ret = tsv_write_header(ectxt, TSV_TYPE_QTIMER, sizeof(t_now)); if (ret) return ret; return tsv_write_data(ectxt, &t_now, sizeof(t_now)); } EXPORT_SYMBOL(tsv_qtimer_write); /* * Writes a data pointer. * * @ectxt context initialized by calling msg_encode_start() * @pointer pointer value to write */ int tsv_pointer_write(struct encode_context *ectxt, void *pointer) { int ret; ret = tsv_write_header(ectxt, TSV_TYPE_POINTER, sizeof(pointer)); if (ret) return ret; return tsv_write_data(ectxt, &pointer, sizeof(pointer)); } EXPORT_SYMBOL(tsv_pointer_write); /* * Writes a 32-bit integer value. * * @ectxt context initialized by calling msg_encode_start() * @n integer to write */ int tsv_int32_write(struct encode_context *ectxt, int32_t n) { int ret; ret = tsv_write_header(ectxt, TSV_TYPE_INT32, sizeof(n)); if (ret) return ret; return tsv_write_data(ectxt, &n, sizeof(n)); } EXPORT_SYMBOL(tsv_int32_write); /* * Writes a byte array. * * @ectxt context initialized by calling msg_write_start() * @data Beginning address of data * @data_size Size of data to be written */ int tsv_byte_array_write(struct encode_context *ectxt, void *data, int data_size) { int ret; ret = tsv_write_header(ectxt, TSV_TYPE_BYTE_ARRAY, data_size); if (ret) return ret; return tsv_write_data(ectxt, data, data_size); } EXPORT_SYMBOL(tsv_byte_array_write); /* * Helper function to log a string * * @ilctxt ipc_log_context created using ipc_log_context_create() * @fmt Data specified using format specifiers */ int ipc_log_string(void *ilctxt, const char *fmt, ...) { struct encode_context ectxt; int avail_size, data_size, hdr_size = sizeof(struct tsv_header); va_list arg_list; if (!ilctxt) return -EINVAL; msg_encode_start(&ectxt, TSV_TYPE_STRING); tsv_timestamp_write(&ectxt); tsv_qtimer_write(&ectxt); avail_size = (MAX_MSG_SIZE - (ectxt.offset + hdr_size)); va_start(arg_list, fmt); data_size = vscnprintf((ectxt.buff + ectxt.offset + hdr_size), avail_size, fmt, arg_list); va_end(arg_list); tsv_write_header(&ectxt, TSV_TYPE_BYTE_ARRAY, data_size); ectxt.offset += data_size; msg_encode_end(&ectxt); ipc_log_write(ilctxt, &ectxt); return 0; } EXPORT_SYMBOL(ipc_log_string); /** * ipc_log_extract - Reads and deserializes log * * @ctxt: logging context * @buff: buffer to receive the data * @size: size of the buffer * @returns: 0 if no data read; >0 number of bytes read; < 0 error * * If no data is available to be read, then the ilctxt::read_avail * completion is reinitialized. This allows clients to block * until new log data is save. */ int ipc_log_extract(void *ctxt, char *buff, int size) { struct encode_context ectxt; struct decode_context dctxt; void (*deserialize_func)(struct encode_context *ectxt, struct decode_context *dctxt); struct ipc_log_context *ilctxt = (struct ipc_log_context *)ctxt; unsigned long flags; int ret; if (size < MAX_MSG_DECODED_SIZE) return -EINVAL; dctxt.output_format = OUTPUT_DEBUGFS; dctxt.buff = buff; dctxt.size = size; read_lock_irqsave(&context_list_lock_lha1, flags); spin_lock(&ilctxt->context_lock_lhb1); if (ilctxt->destroyed) { ret = -EIO; goto done; } while (dctxt.size >= MAX_MSG_DECODED_SIZE && !is_nd_read_empty(ilctxt)) { msg_read(ilctxt, &ectxt); deserialize_func = get_deserialization_func(ilctxt, ectxt.hdr.type); spin_unlock(&ilctxt->context_lock_lhb1); read_unlock_irqrestore(&context_list_lock_lha1, flags); if (deserialize_func) deserialize_func(&ectxt, &dctxt); else pr_err("%s: unknown message 0x%x\n", __func__, ectxt.hdr.type); read_lock_irqsave(&context_list_lock_lha1, flags); spin_lock(&ilctxt->context_lock_lhb1); } ret = size - dctxt.size; if (ret == 0) { if (!ilctxt->destroyed) reinit_completion(&ilctxt->read_avail); else ret = -EIO; } done: spin_unlock(&ilctxt->context_lock_lhb1); read_unlock_irqrestore(&context_list_lock_lha1, flags); return ret; } EXPORT_SYMBOL(ipc_log_extract); /* * Helper function used to read data from a message context. * * @ectxt context initialized by calling msg_read() * @data data to read * @size number of bytes of data to read */ static void tsv_read_data(struct encode_context *ectxt, void *data, uint32_t size) { if (WARN_ON((ectxt->offset + size) > MAX_MSG_SIZE)) { memcpy(data, (ectxt->buff + ectxt->offset), MAX_MSG_SIZE - ectxt->offset - 1); ectxt->offset += MAX_MSG_SIZE - ectxt->offset - 1; return; } memcpy(data, (ectxt->buff + ectxt->offset), size); ectxt->offset += size; } /* * Helper function that reads a type from the context and updates the * context pointers. * * @ectxt context initialized by calling msg_read() * @hdr type header */ static void tsv_read_header(struct encode_context *ectxt, struct tsv_header *hdr) { if (WARN_ON((ectxt->offset + sizeof(*hdr)) > MAX_MSG_SIZE)) { memcpy(hdr, (ectxt->buff + ectxt->offset), MAX_MSG_SIZE - ectxt->offset - 1); ectxt->offset += MAX_MSG_SIZE - ectxt->offset - 1; return; } memcpy(hdr, (ectxt->buff + ectxt->offset), sizeof(*hdr)); ectxt->offset += sizeof(*hdr); } /* * Reads a timestamp. * * @ectxt context initialized by calling msg_read() * @dctxt deserialization context * @format output format (appended to %6u.09u timestamp format) */ void tsv_timestamp_read(struct encode_context *ectxt, struct decode_context *dctxt, const char *format) { struct tsv_header hdr; uint64_t val; unsigned long nanosec_rem; tsv_read_header(ectxt, &hdr); if (WARN_ON(hdr.type != TSV_TYPE_TIMESTAMP)) return; tsv_read_data(ectxt, &val, sizeof(val)); nanosec_rem = do_div(val, 1000000000U); IPC_SPRINTF_DECODE(dctxt, "[%6u.%09lu%s/", (unsigned int)val, nanosec_rem, format); } EXPORT_SYMBOL(tsv_timestamp_read); /* * Reads a QTimer timestamp. * * @ectxt context initialized by calling msg_read() * @dctxt deserialization context * @format output format (appended to %#18llx timestamp format) */ void tsv_qtimer_read(struct encode_context *ectxt, struct decode_context *dctxt, const char *format) { struct tsv_header hdr; uint64_t val; tsv_read_header(ectxt, &hdr); if (WARN_ON(hdr.type != TSV_TYPE_QTIMER)) return; tsv_read_data(ectxt, &val, sizeof(val)); /* * This gives 16 hex digits of output. The # prefix prepends * a 0x, and these characters count as part of the number. */ IPC_SPRINTF_DECODE(dctxt, "%#18llx]%s", val, format); } EXPORT_SYMBOL(tsv_qtimer_read); /* * Reads a data pointer. * * @ectxt context initialized by calling msg_read() * @dctxt deserialization context * @format output format */ void tsv_pointer_read(struct encode_context *ectxt, struct decode_context *dctxt, const char *format) { struct tsv_header hdr; void *val; tsv_read_header(ectxt, &hdr); if (WARN_ON(hdr.type != TSV_TYPE_POINTER)) return; tsv_read_data(ectxt, &val, sizeof(val)); IPC_SPRINTF_DECODE(dctxt, format, val); } EXPORT_SYMBOL(tsv_pointer_read); /* * Reads a 32-bit integer value. * * @ectxt context initialized by calling msg_read() * @dctxt deserialization context * @format output format */ int32_t tsv_int32_read(struct encode_context *ectxt, struct decode_context *dctxt, const char *format) { struct tsv_header hdr; int32_t val; tsv_read_header(ectxt, &hdr); if (WARN_ON(hdr.type != TSV_TYPE_INT32)) return -EINVAL; tsv_read_data(ectxt, &val, sizeof(val)); IPC_SPRINTF_DECODE(dctxt, format, val); return val; } EXPORT_SYMBOL(tsv_int32_read); /* * Reads a byte array/string. * * @ectxt context initialized by calling msg_read() * @dctxt deserialization context * @format output format */ void tsv_byte_array_read(struct encode_context *ectxt, struct decode_context *dctxt, const char *format) { struct tsv_header hdr; tsv_read_header(ectxt, &hdr); if (WARN_ON(hdr.type != TSV_TYPE_BYTE_ARRAY)) return; tsv_read_data(ectxt, dctxt->buff, hdr.size); dctxt->buff += hdr.size; dctxt->size -= hdr.size; } EXPORT_SYMBOL(tsv_byte_array_read); int add_deserialization_func(void *ctxt, int type, void (*dfunc)(struct encode_context *, struct decode_context *)) { struct ipc_log_context *ilctxt = (struct ipc_log_context *)ctxt; struct dfunc_info *df_info; unsigned long flags; if (!ilctxt || !dfunc) return -EINVAL; df_info = kmalloc(sizeof(struct dfunc_info), GFP_KERNEL); if (!df_info) return -ENOSPC; read_lock_irqsave(&context_list_lock_lha1, flags); spin_lock(&ilctxt->context_lock_lhb1); df_info->type = type; df_info->dfunc = dfunc; list_add_tail(&df_info->list, &ilctxt->dfunc_info_list); spin_unlock(&ilctxt->context_lock_lhb1); read_unlock_irqrestore(&context_list_lock_lha1, flags); return 0; } EXPORT_SYMBOL(add_deserialization_func); static void *get_deserialization_func(struct ipc_log_context *ilctxt, int type) { struct dfunc_info *df_info = NULL; if (!ilctxt) return NULL; list_for_each_entry(df_info, &ilctxt->dfunc_info_list, list) { if (df_info->type == type) return df_info->dfunc; } return NULL; } /** * ipc_log_context_create: Create a debug log context if context does not exist. * Should not be called from atomic context * * @max_num_pages: Number of pages of logging space required (max. 10) * @mod_name : Name of the directory entry under DEBUGFS * @feature_version : First 16 bit for version number of user-defined message * formats and next 16 bit for enabling minidump * * returns context id on success, NULL on failure */ void *ipc_log_context_create(int max_num_pages, const char *mod_name, uint32_t feature_version) { struct ipc_log_context *ctxt = NULL, *tmp; struct ipc_log_page *pg = NULL; int page_cnt; unsigned long flags; int enable_minidump; /* check if ipc ctxt already exists */ read_lock_irq(&context_list_lock_lha1); list_for_each_entry(tmp, &ipc_log_context_list, list) if (!strcmp(tmp->name, mod_name)) { ctxt = tmp; break; } read_unlock_irq(&context_list_lock_lha1); if (ctxt) return ctxt; ctxt = kzalloc(sizeof(struct ipc_log_context), GFP_KERNEL); if (!ctxt) return NULL; init_completion(&ctxt->read_avail); INIT_LIST_HEAD(&ctxt->page_list); INIT_LIST_HEAD(&ctxt->dfunc_info_list); spin_lock_init(&ctxt->context_lock_lhb1); enable_minidump = feature_version & FEATURE_MASK; spin_lock_irqsave(&ctxt->context_lock_lhb1, flags); if (enable_minidump) { register_minidump((u64)ctxt, sizeof(struct ipc_log_context), "ipc_ctxt", minidump_buf_cnt); } spin_unlock_irqrestore(&ctxt->context_lock_lhb1, flags); for (page_cnt = 0; page_cnt < max_num_pages; page_cnt++) { pg = kzalloc(sizeof(struct ipc_log_page), GFP_KERNEL); if (!pg) goto release_ipc_log_context; pg->hdr.log_id = (uint64_t)(uintptr_t)ctxt; pg->hdr.page_num = LOG_PAGE_FLAG | page_cnt; pg->hdr.ctx_offset = (int64_t)((uint64_t)(uintptr_t)ctxt - (uint64_t)(uintptr_t)&pg->hdr); /* set magic last to signal that page init is complete */ pg->hdr.magic = IPC_LOGGING_MAGIC_NUM; pg->hdr.nmagic = ~(IPC_LOGGING_MAGIC_NUM); spin_lock_irqsave(&ctxt->context_lock_lhb1, flags); list_add_tail(&pg->hdr.list, &ctxt->page_list); if (enable_minidump) { register_minidump((u64)pg, sizeof(struct ipc_log_page), mod_name, minidump_buf_cnt); } spin_unlock_irqrestore(&ctxt->context_lock_lhb1, flags); } ctxt->log_id = (uint64_t)(uintptr_t)ctxt; ctxt->version = IPC_LOG_VERSION; strlcpy(ctxt->name, mod_name, IPC_LOG_MAX_CONTEXT_NAME_LEN); ctxt->user_version = feature_version & 0xffff; ctxt->first_page = get_first_page(ctxt); ctxt->last_page = pg; ctxt->write_page = ctxt->first_page; ctxt->read_page = ctxt->first_page; ctxt->nd_read_page = ctxt->first_page; ctxt->write_avail = max_num_pages * LOG_PAGE_DATA_SIZE; ctxt->header_size = sizeof(struct ipc_log_page_header); kref_init(&ctxt->refcount); ctxt->destroyed = false; create_ctx_debugfs(ctxt, mod_name); /* set magic last to signal context init is complete */ ctxt->magic = IPC_LOG_CONTEXT_MAGIC_NUM; ctxt->nmagic = ~(IPC_LOG_CONTEXT_MAGIC_NUM); write_lock_irqsave(&context_list_lock_lha1, flags); if (enable_minidump && (minidump_buf_cnt < MAX_MINIDUMP_BUFFERS)) list_add(&ctxt->list, &ipc_log_context_list); else list_add_tail(&ctxt->list, &ipc_log_context_list); write_unlock_irqrestore(&context_list_lock_lha1, flags); return (void *)ctxt; release_ipc_log_context: while (page_cnt-- > 0) { pg = get_first_page(ctxt); list_del(&pg->hdr.list); kfree(pg); } kfree(ctxt); return NULL; } EXPORT_SYMBOL(ipc_log_context_create); void ipc_log_context_free(struct kref *kref) { struct ipc_log_context *ilctxt = container_of(kref, struct ipc_log_context, refcount); struct ipc_log_page *pg = NULL; while (!list_empty(&ilctxt->page_list)) { pg = get_first_page(ilctxt); list_del(&pg->hdr.list); kfree(pg); } kfree(ilctxt); } /* * Destroy debug log context * * @ctxt: debug log context created by calling ipc_log_context_create API. */ int ipc_log_context_destroy(void *ctxt) { struct ipc_log_context *ilctxt = (struct ipc_log_context *)ctxt; struct dfunc_info *df_info = NULL, *tmp = NULL; unsigned long flags; if (!ilctxt) return 0; debugfs_remove_recursive(ilctxt->dent); spin_lock(&ilctxt->context_lock_lhb1); ilctxt->destroyed = true; complete_all(&ilctxt->read_avail); list_for_each_entry_safe(df_info, tmp, &ilctxt->dfunc_info_list, list) { list_del(&df_info->list); kfree(df_info); } spin_unlock(&ilctxt->context_lock_lhb1); write_lock_irqsave(&context_list_lock_lha1, flags); list_del(&ilctxt->list); write_unlock_irqrestore(&context_list_lock_lha1, flags); ipc_log_context_put(ilctxt); return 0; } EXPORT_SYMBOL(ipc_log_context_destroy); #endif static int cnss_pin_connect_show(struct seq_file *s, void *data) { struct cnss_plat_data *cnss_priv = s->private; seq_puts(s, "Pin connect results\n"); seq_printf(s, "FW power pin result: %04x\n", cnss_priv->pin_result.fw_pwr_pin_result); seq_printf(s, "FW PHY IO pin result: %04x\n", cnss_priv->pin_result.fw_phy_io_pin_result); seq_printf(s, "FW RF pin result: %04x\n", cnss_priv->pin_result.fw_rf_pin_result); seq_printf(s, "Host pin result: %04x\n", cnss_priv->pin_result.host_pin_result); seq_puts(s, "\n"); return 0; } static int cnss_pin_connect_open(struct inode *inode, struct file *file) { return single_open(file, cnss_pin_connect_show, inode->i_private); } static const struct file_operations cnss_pin_connect_fops = { .read = seq_read, .release = single_release, .open = cnss_pin_connect_open, .owner = THIS_MODULE, .llseek = seq_lseek, }; static int cnss_stats_show_state(struct seq_file *s, struct cnss_plat_data *plat_priv) { enum cnss_driver_state i; int skip = 0; unsigned long state; seq_printf(s, "\nState: 0x%lx(", plat_priv->driver_state); for (i = 0, state = plat_priv->driver_state; state != 0; state >>= 1, i++) { if (!(state & 0x1)) continue; if (skip++) seq_puts(s, " | "); switch (i) { case CNSS_QMI_WLFW_CONNECTED: seq_puts(s, "QMI_WLFW_CONNECTED"); continue; case CNSS_FW_MEM_READY: seq_puts(s, "FW_MEM_READY"); continue; case CNSS_FW_READY: seq_puts(s, "FW_READY"); continue; case CNSS_COLD_BOOT_CAL: seq_puts(s, "COLD_BOOT_CAL"); continue; case CNSS_DRIVER_LOADING: seq_puts(s, "DRIVER_LOADING"); continue; case CNSS_DRIVER_UNLOADING: seq_puts(s, "DRIVER_UNLOADING"); continue; case CNSS_DRIVER_IDLE_RESTART: seq_puts(s, "IDLE_RESTART"); continue; case CNSS_DRIVER_IDLE_SHUTDOWN: seq_puts(s, "IDLE_SHUTDOWN"); continue; case CNSS_DRIVER_PROBED: seq_puts(s, "DRIVER_PROBED"); continue; case CNSS_DRIVER_RECOVERY: seq_puts(s, "DRIVER_RECOVERY"); continue; case CNSS_FW_BOOT_RECOVERY: seq_puts(s, "FW_BOOT_RECOVERY"); continue; case CNSS_DEV_ERR_NOTIFY: seq_puts(s, "DEV_ERR"); continue; case CNSS_DRIVER_DEBUG: seq_puts(s, "DRIVER_DEBUG"); continue; case CNSS_COEX_CONNECTED: seq_puts(s, "COEX_CONNECTED"); continue; case CNSS_IMS_CONNECTED: seq_puts(s, "IMS_CONNECTED"); continue; case CNSS_IN_SUSPEND_RESUME: seq_puts(s, "IN_SUSPEND_RESUME"); continue; case CNSS_DAEMON_CONNECTED: seq_puts(s, "DAEMON_CONNECTED"); continue; case CNSS_QDSS_STARTED: seq_puts(s, "QDSS_STARTED"); continue; case CNSS_RECOVERY_WAIT_FOR_DRIVER: seq_puts(s, "CNSS_RECOVERY_WAIT_FOR_DRIVER"); continue; case CNSS_RDDM_DUMP_IN_PROGRESS: seq_puts(s, "RDDM_DUMP_IN_PROGRESS"); continue; } seq_printf(s, "UNKNOWN-%d", i); } seq_puts(s, ")\n"); return 0; } static int cnss_stats_show(struct seq_file *s, void *data) { struct cnss_plat_data *plat_priv = s->private; cnss_stats_show_state(s, plat_priv); return 0; } static int cnss_stats_open(struct inode *inode, struct file *file) { return single_open(file, cnss_stats_show, inode->i_private); } static const struct file_operations cnss_stats_fops = { .read = seq_read, .release = single_release, .open = cnss_stats_open, .owner = THIS_MODULE, .llseek = seq_lseek, }; static int cnss_debug_probe(struct pci_dev *pdev, const struct pci_device_id *id) { struct platform_device *plat_dev = (struct platform_device *)pdev; struct cnss_plat_data *plat_priv = cnss_get_plat_priv(plat_dev); struct pci_dev *pci_dev = plat_priv->pci_dev; cnss_pr_err("%s: %d: plat_priv %pK device %pK\n", __func__, __LINE__, plat_priv, pci_dev); cnss_wait_for_fw_ready(&pci_dev->dev); return 0; } static void cnss_debug_remove(struct pci_dev *pdev) { struct platform_device *plat_dev = (struct platform_device *)pdev; struct cnss_plat_data *plat_priv = cnss_get_plat_priv(plat_dev); cnss_pr_err("%s: %d: plat_priv %pK\n", __func__, __LINE__, plat_priv); } static void cnss_debug_shutdown(struct pci_dev *pdev) { struct platform_device *plat_dev = (struct platform_device *)pdev; struct cnss_plat_data *plat_priv = cnss_get_plat_priv(plat_dev); cnss_pr_err("%s: %d: plat_priv %pK\n", __func__, __LINE__, plat_priv); } static void cnss_debug_update_status(struct pci_dev *pdev, const struct pci_device_id *id, int status) { struct platform_device *plat_dev = (struct platform_device *)pdev; struct cnss_plat_data *plat_priv = cnss_get_plat_priv(plat_dev); cnss_pr_err("%s: %d: plat_priv %pK status %d\n", __func__, __LINE__, plat_priv, status); } static int cnss_debug_fatal(struct pci_dev *pdev, const struct pci_device_id *id) { struct platform_device *plat_dev = (struct platform_device *)pdev; struct cnss_plat_data *plat_priv = cnss_get_plat_priv(plat_dev); cnss_pr_err("%s: %d: device %x\n", __func__, __LINE__, id->device); return 0; } struct cnss_wlan_driver debug_driver_ops = { .name = "pld_pcie", .probe = cnss_debug_probe, .remove = cnss_debug_remove, .update_status = cnss_debug_update_status, .fatal = cnss_debug_fatal, .shutdown = cnss_debug_shutdown, }; static ssize_t cnss_dev_boot_debug_write(struct file *fp, const char __user *user_buf, size_t count, loff_t *off) { struct cnss_plat_data *plat_priv = ((struct seq_file *)fp->private_data)->private; struct cnss_pci_data *pci_priv; char buf[64]; char *cmd; unsigned int len = 0; int ret = 0; len = min(count, sizeof(buf) - 1); if (copy_from_user(buf, user_buf, len)) return -EFAULT; buf[len] = '\0'; cmd = (char *)buf; if (sysfs_streq("test_driver_load", cmd)) { ret = cnss_wlan_register_driver_ops(&debug_driver_ops); if (ret) { cnss_pr_err("Fail to register driver ops\n"); return ret; } ret = cnss_wlan_probe_driver(); if (ret) { cnss_pr_err("Fail to register driver probe\n"); return ret; } return count; } if (!plat_priv) return -ENODEV; pci_priv = plat_priv->bus_priv; if (!pci_priv) return -ENODEV; if (sysfs_streq("on", cmd)) { ret = cnss_power_on_device(plat_priv, 0); } else if (sysfs_streq("off", cmd)) { cnss_power_off_device(plat_priv, 0); } else if (sysfs_streq("enumerate", cmd)) { ret = cnss_pci_init(plat_priv); } else if (sysfs_streq("download", cmd)) { set_bit(CNSS_DRIVER_DEBUG, &plat_priv->driver_state); ret = cnss_pci_start_mhi(pci_priv); } else if (sysfs_streq("linkup", cmd)) { ret = cnss_resume_pci_link(pci_priv); } else if (sysfs_streq("linkdown", cmd)) { ret = cnss_suspend_pci_link(pci_priv); } else if (sysfs_streq("powerup", cmd)) { set_bit(CNSS_DRIVER_DEBUG, &plat_priv->driver_state); ret = cnss_driver_event_post(plat_priv, CNSS_DRIVER_EVENT_POWER_UP, CNSS_EVENT_SYNC, NULL); } else if (sysfs_streq("shutdown", cmd)) { ret = cnss_driver_event_post(plat_priv, CNSS_DRIVER_EVENT_POWER_DOWN, 0, NULL); clear_bit(CNSS_DRIVER_DEBUG, &plat_priv->driver_state); } else if (sysfs_streq("assert", cmd)) { ret = cnss_force_fw_assert(&pci_priv->pci_dev->dev); } else { cnss_pr_err("Device boot debugfs command is invalid\n"); ret = -EINVAL; } if (ret) return ret; return count; } static int cnss_dev_boot_debug_show(struct seq_file *s, void *data) { seq_puts(s, "\nUsage: echo > /cnss/dev_boot\n"); seq_puts(s, " can be one of below:\n"); seq_puts(s, "on: turn on device power, assert WLAN_EN\n"); seq_puts(s, "off: de-assert WLAN_EN, turn off device power\n"); seq_puts(s, "enumerate: de-assert PERST, enumerate PCIe\n"); seq_puts(s, "download: download FW and do QMI handshake with FW\n"); seq_puts(s, "linkup: bring up PCIe link\n"); seq_puts(s, "linkdown: bring down PCIe link\n"); seq_puts(s, "powerup: full power on sequence to boot device, download FW and do QMI handshake with FW\n"); seq_puts(s, "shutdown: full power off sequence to shutdown device\n"); seq_puts(s, "assert: trigger firmware assert\n"); return 0; } static int cnss_dev_boot_debug_open(struct inode *inode, struct file *file) { return single_open(file, cnss_dev_boot_debug_show, inode->i_private); } static const struct file_operations cnss_dev_boot_debug_fops = { .read = seq_read, .write = cnss_dev_boot_debug_write, .release = single_release, .open = cnss_dev_boot_debug_open, .owner = THIS_MODULE, .llseek = seq_lseek, }; static int cnss_reg_read_debug_show(struct seq_file *s, void *data) { struct cnss_plat_data *plat_priv = s->private; mutex_lock(&plat_priv->dev_lock); if (!plat_priv->diag_reg_read_buf) { seq_puts(s, "\nUsage: echo > /cnss/reg_read\n"); mutex_unlock(&plat_priv->dev_lock); return 0; } seq_printf(s, "\nRegister read, address: 0x%x memory type: 0x%x length: 0x%x\n\n", plat_priv->diag_reg_read_addr, plat_priv->diag_reg_read_mem_type, plat_priv->diag_reg_read_len); seq_hex_dump(s, "", DUMP_PREFIX_OFFSET, 32, 4, plat_priv->diag_reg_read_buf, plat_priv->diag_reg_read_len, false); plat_priv->diag_reg_read_len = 0; kfree(plat_priv->diag_reg_read_buf); plat_priv->diag_reg_read_buf = NULL; mutex_unlock(&plat_priv->dev_lock); return 0; } static ssize_t cnss_reg_read_debug_write(struct file *fp, const char __user *user_buf, size_t count, loff_t *off) { struct cnss_plat_data *plat_priv = ((struct seq_file *)fp->private_data)->private; char buf[64]; char *sptr, *token; unsigned int len = 0; u32 reg_offset, mem_type; u32 data_len = 0; u8 *reg_buf = NULL; const char *delim = " "; int ret = 0; if (!test_bit(CNSS_FW_READY, &plat_priv->driver_state)) { cnss_pr_err("Firmware is not ready yet\n"); return -EINVAL; } len = min(count, sizeof(buf) - 1); if (copy_from_user(buf, user_buf, len)) return -EFAULT; buf[len] = '\0'; sptr = buf; token = strsep(&sptr, delim); if (!token) return -EINVAL; if (!sptr) return -EINVAL; if (kstrtou32(token, 0, &mem_type)) return -EINVAL; token = strsep(&sptr, delim); if (!token) return -EINVAL; if (!sptr) return -EINVAL; if (kstrtou32(token, 0, ®_offset)) return -EINVAL; token = strsep(&sptr, delim); if (!token) return -EINVAL; if (kstrtou32(token, 0, &data_len)) return -EINVAL; mutex_lock(&plat_priv->dev_lock); kfree(plat_priv->diag_reg_read_buf); plat_priv->diag_reg_read_buf = NULL; reg_buf = kzalloc(data_len, GFP_KERNEL); if (!reg_buf) { mutex_unlock(&plat_priv->dev_lock); return -ENOMEM; } ret = cnss_wlfw_athdiag_read_send_sync(plat_priv, reg_offset, mem_type, data_len, reg_buf); if (ret) { kfree(reg_buf); mutex_unlock(&plat_priv->dev_lock); return ret; } plat_priv->diag_reg_read_addr = reg_offset; plat_priv->diag_reg_read_mem_type = mem_type; plat_priv->diag_reg_read_len = data_len; plat_priv->diag_reg_read_buf = reg_buf; mutex_unlock(&plat_priv->dev_lock); return count; } static int cnss_reg_read_debug_open(struct inode *inode, struct file *file) { return single_open(file, cnss_reg_read_debug_show, inode->i_private); } static const struct file_operations cnss_reg_read_debug_fops = { .read = seq_read, .write = cnss_reg_read_debug_write, .open = cnss_reg_read_debug_open, .owner = THIS_MODULE, .llseek = seq_lseek, }; static int cnss_reg_write_debug_show(struct seq_file *s, void *data) { seq_puts(s, "\nUsage: echo > /cnss/reg_write\n"); return 0; } static ssize_t cnss_reg_write_debug_write(struct file *fp, const char __user *user_buf, size_t count, loff_t *off) { struct cnss_plat_data *plat_priv = ((struct seq_file *)fp->private_data)->private; char buf[64]; char *sptr, *token; unsigned int len = 0; u32 reg_offset, mem_type, reg_val; const char *delim = " "; int ret = 0; if (!test_bit(CNSS_FW_READY, &plat_priv->driver_state)) { cnss_pr_err("Firmware is not ready yet\n"); return -EINVAL; } len = min(count, sizeof(buf) - 1); if (copy_from_user(buf, user_buf, len)) return -EFAULT; buf[len] = '\0'; sptr = buf; token = strsep(&sptr, delim); if (!token) return -EINVAL; if (!sptr) return -EINVAL; if (kstrtou32(token, 0, &mem_type)) return -EINVAL; token = strsep(&sptr, delim); if (!token) return -EINVAL; if (!sptr) return -EINVAL; if (kstrtou32(token, 0, ®_offset)) return -EINVAL; token = strsep(&sptr, delim); if (!token) return -EINVAL; if (kstrtou32(token, 0, ®_val)) return -EINVAL; ret = cnss_wlfw_athdiag_write_send_sync(plat_priv, reg_offset, mem_type, sizeof(u32), (u8 *)®_val); if (ret) return ret; return count; } static int cnss_reg_write_debug_open(struct inode *inode, struct file *file) { return single_open(file, cnss_reg_write_debug_show, inode->i_private); } static const struct file_operations cnss_reg_write_debug_fops = { .read = seq_read, .write = cnss_reg_write_debug_write, .open = cnss_reg_write_debug_open, .owner = THIS_MODULE, .llseek = seq_lseek, }; static ssize_t cnss_runtime_pm_debug_write(struct file *fp, const char __user *user_buf, size_t count, loff_t *off) { struct cnss_plat_data *plat_priv = ((struct seq_file *)fp->private_data)->private; struct cnss_pci_data *pci_priv; char buf[64]; char *cmd; unsigned int len = 0; int ret = 0; if (!plat_priv) return -ENODEV; pci_priv = plat_priv->bus_priv; if (!pci_priv) return -ENODEV; len = min(count, sizeof(buf) - 1); if (copy_from_user(buf, user_buf, len)) return -EFAULT; buf[len] = '\0'; cmd = buf; if (sysfs_streq("usage_count", cmd)) { cnss_pci_pm_runtime_show_usage_count(pci_priv); } else if (sysfs_streq("request_resume", cmd)) { ret = cnss_pci_pm_request_resume(pci_priv); } else if (sysfs_streq("resume", cmd)) { ret = cnss_pci_pm_runtime_resume(pci_priv); } else if (sysfs_streq("get", cmd)) { ret = cnss_pci_pm_runtime_get(pci_priv); } else if (sysfs_streq("get_noresume", cmd)) { cnss_pci_pm_runtime_get_noresume(pci_priv); } else if (sysfs_streq("put_autosuspend", cmd)) { ret = cnss_pci_pm_runtime_put_autosuspend(pci_priv); } else if (sysfs_streq("put_noidle", cmd)) { cnss_pci_pm_runtime_put_noidle(pci_priv); } else if (sysfs_streq("mark_last_busy", cmd)) { cnss_pci_pm_runtime_mark_last_busy(pci_priv); } else { cnss_pr_err("Runtime PM debugfs command is invalid\n"); ret = -EINVAL; } if (ret) return ret; return count; } static int cnss_runtime_pm_debug_show(struct seq_file *s, void *data) { seq_puts(s, "\nUsage: echo > /cnss/runtime_pm\n"); seq_puts(s, " can be one of below:\n"); seq_puts(s, "usage_count: get runtime PM usage count\n"); seq_puts(s, "get: do runtime PM get\n"); seq_puts(s, "get_noresume: do runtime PM get noresume\n"); seq_puts(s, "put_noidle: do runtime PM put noidle\n"); seq_puts(s, "put_autosuspend: do runtime PM put autosuspend\n"); seq_puts(s, "mark_last_busy: do runtime PM mark last busy\n"); return 0; } static int cnss_runtime_pm_debug_open(struct inode *inode, struct file *file) { return single_open(file, cnss_runtime_pm_debug_show, inode->i_private); } static const struct file_operations cnss_runtime_pm_debug_fops = { .read = seq_read, .write = cnss_runtime_pm_debug_write, .open = cnss_runtime_pm_debug_open, .owner = THIS_MODULE, .llseek = seq_lseek, }; static ssize_t cnss_control_params_debug_write(struct file *fp, const char __user *user_buf, size_t count, loff_t *off) { struct cnss_plat_data *plat_priv = ((struct seq_file *)fp->private_data)->private; char buf[64]; char *sptr, *token; char *cmd; u32 val; unsigned int len = 0; const char *delim = " "; if (!plat_priv) return -ENODEV; len = min(count, sizeof(buf) - 1); if (copy_from_user(buf, user_buf, len)) return -EFAULT; buf[len] = '\0'; sptr = buf; token = strsep(&sptr, delim); if (!token) return -EINVAL; if (!sptr) return -EINVAL; cmd = token; token = strsep(&sptr, delim); if (!token) return -EINVAL; if (kstrtou32(token, 0, &val)) return -EINVAL; if (strcmp(cmd, "quirks") == 0) plat_priv->ctrl_params.quirks = val; else if (strcmp(cmd, "mhi_timeout") == 0) plat_priv->ctrl_params.mhi_timeout = val; else if (strcmp(cmd, "qmi_timeout") == 0) plat_priv->ctrl_params.qmi_timeout = val; else if (strcmp(cmd, "bdf_type") == 0) plat_priv->ctrl_params.bdf_type = val; else if (strcmp(cmd, "time_sync_period") == 0) plat_priv->ctrl_params.time_sync_period = val; else return -EINVAL; return count; } static int cnss_show_quirks_state(struct seq_file *s, struct cnss_plat_data *plat_priv) { enum cnss_debug_quirks i; int skip = 0; unsigned long state; seq_printf(s, "quirks: 0x%lx (", plat_priv->ctrl_params.quirks); for (i = 0, state = plat_priv->ctrl_params.quirks; state != 0; state >>= 1, i++) { if (!(state & 0x1)) continue; if (skip++) seq_puts(s, " | "); switch (i) { case LINK_DOWN_SELF_RECOVERY: seq_puts(s, "LINK_DOWN_SELF_RECOVERY"); continue; case SKIP_DEVICE_BOOT: seq_puts(s, "SKIP_DEVICE_BOOT"); continue; case USE_CORE_ONLY_FW: seq_puts(s, "USE_CORE_ONLY_FW"); continue; case SKIP_RECOVERY: seq_puts(s, "SKIP_RECOVERY"); continue; case QMI_BYPASS: seq_puts(s, "QMI_BYPASS"); continue; case ENABLE_WALTEST: seq_puts(s, "WALTEST"); continue; case ENABLE_PCI_LINK_DOWN_PANIC: seq_puts(s, "PCI_LINK_DOWN_PANIC"); continue; case FBC_BYPASS: seq_puts(s, "FBC_BYPASS"); continue; case ENABLE_DAEMON_SUPPORT: seq_puts(s, "DAEMON_SUPPORT"); continue; case DISABLE_DRV: seq_puts(s, "DISABLE_DRV"); continue; } seq_printf(s, "UNKNOWN-%d", i); } seq_puts(s, ")\n"); return 0; } static int cnss_control_params_debug_show(struct seq_file *s, void *data) { struct cnss_plat_data *cnss_priv = s->private; seq_puts(s, "\nUsage: echo > /cnss/control_params\n"); seq_puts(s, " can be one of below:\n"); seq_puts(s, "quirks: Debug quirks for driver\n"); seq_puts(s, "mhi_timeout: Timeout for MHI operation in milliseconds\n"); seq_puts(s, "qmi_timeout: Timeout for QMI message in milliseconds\n"); seq_puts(s, "bdf_type: Type of board data file to be downloaded\n"); seq_puts(s, "time_sync_period: Time period to do time sync with device in milliseconds\n"); seq_puts(s, "\nCurrent value:\n"); cnss_show_quirks_state(s, cnss_priv); seq_printf(s, "mhi_timeout: %u\n", cnss_priv->ctrl_params.mhi_timeout); seq_printf(s, "qmi_timeout: %u\n", cnss_priv->ctrl_params.qmi_timeout); seq_printf(s, "bdf_type: %u\n", cnss_priv->ctrl_params.bdf_type); seq_printf(s, "time_sync_period: %u\n", cnss_priv->ctrl_params.time_sync_period); return 0; } static int cnss_control_params_debug_open(struct inode *inode, struct file *file) { return single_open(file, cnss_control_params_debug_show, inode->i_private); } static const struct file_operations cnss_control_params_debug_fops = { .read = seq_read, .write = cnss_control_params_debug_write, .open = cnss_control_params_debug_open, .owner = THIS_MODULE, .llseek = seq_lseek, }; static ssize_t cnss_ce_reg_info_debug_write(struct file *fp, const char __user *user_buf, size_t count, loff_t *off) { u64 ce_bitmask; int ret; int ce; struct cnss_ce_base_addr *ce_object; struct cnss_plat_data *plat_priv = ((struct seq_file *)fp->private_data)->private; ret = kstrtou64_from_user(user_buf, count, 16, &ce_bitmask); if (ret) return ret; ce_object = register_ce_object(plat_priv); if (!ce_object) { cnss_pr_err("CE object is null\n"); return -1; } if (ce_bitmask >= (1 << ce_object->max_ce_count)) return -EINVAL; for (ce = 0; ce < ce_object->max_ce_count; ce++) { /* Each bit represents CEx register. The user can also dump the * specific CE register(s). * e.g: echo 0x5 > ce_info will dump CE0 and CE2 registers. */ if (ce_bitmask & (1 << ce)) cnss_dump_ce_reg(plat_priv, ce, ce_object); } return count; } static int cnss_ce_reg_info_debug_show(struct seq_file *s, void *data) { struct cnss_plat_data *plat_priv = s->private; cnss_pr_info("To print specific CEs, echo bitmask > ce_info\n\n"); cnss_dump_all_ce_reg(plat_priv); return 0; } static int cnss_ce_reg_info_debug_open(struct inode *inode, struct file *file) { return single_open(file, cnss_ce_reg_info_debug_show, inode->i_private); } static const struct file_operations cnss_ce_reg_debug_fops = { .read = seq_read, .write = cnss_ce_reg_info_debug_write, .open = cnss_ce_reg_info_debug_open, .owner = THIS_MODULE, .llseek = seq_lseek, }; static ssize_t cnss_dynamic_feature_write(struct file *fp, const char __user *user_buf, size_t count, loff_t *off) { struct cnss_plat_data *plat_priv = ((struct seq_file *)fp->private_data)->private; int ret = 0; u64 val; ret = kstrtou64_from_user(user_buf, count, 0, &val); if (ret) return ret; plat_priv->dynamic_feature = val; ret = cnss_wlfw_dynamic_feature_mask_send_sync(plat_priv); if (ret < 0) return ret; return count; } static int cnss_dynamic_feature_show(struct seq_file *s, void *data) { struct cnss_plat_data *cnss_priv = s->private; seq_printf(s, "dynamic_feature: 0x%llx\n", cnss_priv->dynamic_feature); return 0; } static int cnss_dynamic_feature_open(struct inode *inode, struct file *file) { return single_open(file, cnss_dynamic_feature_show, inode->i_private); } static const struct file_operations cnss_dynamic_feature_fops = { .read = seq_read, .write = cnss_dynamic_feature_write, .open = cnss_dynamic_feature_open, .owner = THIS_MODULE, .llseek = seq_lseek, }; static ssize_t cnss_hds_support_write(struct file *fp, const char __user *user_buf, size_t count, loff_t *off) { struct cnss_plat_data *plat_priv = ((struct seq_file *)fp->private_data)->private; int ret = 0; u32 val; ret = kstrtou32_from_user(user_buf, count, 0, &val); if (ret) return ret; plat_priv->hds_support = !!val; return count; } static int cnss_hds_support_show(struct seq_file *s, void *data) { struct cnss_plat_data *plat_priv = s->private; seq_printf(s, "hds_support: %s\n", plat_priv->hds_support ? "true" : "false"); return 0; } static int cnss_hds_support_open(struct inode *inode, struct file *file) { return single_open(file, cnss_hds_support_show, inode->i_private); } static const struct file_operations cnss_hds_support_fops = { .read = seq_read, .write = cnss_hds_support_write, .open = cnss_hds_support_open, .owner = THIS_MODULE, .llseek = seq_lseek, }; static ssize_t cnss_qmi_record_debug_write(struct file *fp, const char __user *user_buf, size_t count, loff_t *off) { char buf[4]; if (copy_from_user(buf, user_buf, 4)) return -EFAULT; qmi_record(buf[0], 0xD000 | buf[1], buf[2], buf[3]); return count; } static int cnss_qmi_record_debug_show(struct seq_file *s, void *data) { cnss_dump_qmi_history(); return 0; } static int cnss_qmi_record_debug_open(struct inode *inode, struct file *file) { return single_open(file, cnss_qmi_record_debug_show, inode->i_private); } static const struct file_operations cnss_qmi_record_debug_fops = { .read = seq_read, .write = cnss_qmi_record_debug_write, .release = single_release, .open = cnss_qmi_record_debug_open, .owner = THIS_MODULE, .llseek = seq_lseek, }; #if !defined(CONFIG_CNSS2_KERNEL_5_15) static ssize_t cnss_pci_write_switch_link(struct file *fp, const char __user *user_buf, size_t count, loff_t *off) { struct cnss_plat_data *plat_priv = fp->private_data; u16 link_speed = 0, link_width = 0, pci_set_val = 0; if (kstrtou16_from_user(user_buf, count, 0, &pci_set_val)) return -EFAULT; if (!plat_priv) return -ENODEV; /* The first nibble in a byte represents both pci link speed * width. The first 2[0,1] bits in a nibble represent link speed. * The next 2[2,3] bits represents link width. * e.g: echo 0xB > pci_switch_link will set the PCI Generation * to 3 and PCI lane width to 2. The possible values are, * 1 <= Link speed <= 3 * 1 <= Link width <= 2 */ link_speed = pci_set_val & CNSS_PCI_SWITCH_LINK_MASK; link_width = (pci_set_val >> 2) & CNSS_PCI_SWITCH_LINK_MASK; if (!link_speed || !link_width) { cnss_pr_info("Invalid data\n"); return -EFAULT; } cnss_set_pci_link_speed_width(&plat_priv->plat_dev->dev, link_speed, link_width); return count; } static ssize_t cnss_pci_read_switch_link(struct file *file, char __user *user_buf, size_t count, loff_t *ppos) { const char buf[] = "PCI SWITCH LINk USAGE :\n" "The first 2[0,1] bits in a nibble represent link speed.\n" "The next 2[2,3] bits represents link width.\n" "echo 0xB > pci_switch_link ,will set the PCI Generation\n" "to 3 and PCI lane width to 2. The possible values are,\n" "1 <= Link speed <= 3\n" "1 <= Link width <= 2\n"; return simple_read_from_buffer(user_buf, count, ppos, buf, strlen(buf)); } static const struct file_operations cnss_pci_switch_link_fops = { .read = cnss_pci_read_switch_link, .write = cnss_pci_write_switch_link, .release = single_release, .open = simple_open, .owner = THIS_MODULE, .llseek = seq_lseek, }; #endif static int cnss_mlo_config_debug_show(struct seq_file *s, void *data) { cnss_print_mlo_config(); return 0; } static int cnss_mlo_config_debug_open(struct inode *inode, struct file *file) { return single_open(file, cnss_mlo_config_debug_show, inode->i_private); } static const struct file_operations cnss_mlo_config_debug_fops = { .read = seq_read, .release = single_release, .open = cnss_mlo_config_debug_open, .owner = THIS_MODULE, .llseek = seq_lseek, }; #if 1 static int cnss_cpr_fuse_debug_show(struct seq_file *s, void *data) { struct cnss_plat_data *cnss_priv = s->private; if (cnss_priv && cnss_priv->device_id == QCN9224_DEVICE_ID) { struct cnss_pci_data *pci_priv = cnss_priv->bus_priv; return cnss_pci_print_cpr_fuse(pci_priv, s); } return 0; } static int cnss_cpr_fuse_debug_open(struct inode *inode, struct file *file) { return single_open(file, cnss_cpr_fuse_debug_show, inode->i_private); } static const struct file_operations cnss_cpr_fuse_debug_fops = { .read = seq_read, .release = single_release, .open = cnss_cpr_fuse_debug_open, .owner = THIS_MODULE, .llseek = seq_lseek, }; #endif static int cnss_create_debug_only_node(struct cnss_plat_data *plat_priv) { struct dentry *root_dentry = plat_priv->root_dentry; debugfs_create_file("dev_boot", 0600, root_dentry, plat_priv, &cnss_dev_boot_debug_fops); debugfs_create_file("reg_read", 0600, root_dentry, plat_priv, &cnss_reg_read_debug_fops); debugfs_create_file("reg_write", 0600, root_dentry, plat_priv, &cnss_reg_write_debug_fops); debugfs_create_file("runtime_pm", 0600, root_dentry, plat_priv, &cnss_runtime_pm_debug_fops); debugfs_create_file("control_params", 0600, root_dentry, plat_priv, &cnss_control_params_debug_fops); debugfs_create_file("dynamic_feature", 0600, root_dentry, plat_priv, &cnss_dynamic_feature_fops); debugfs_create_file("hds_support", 0600, root_dentry, plat_priv, &cnss_hds_support_fops); debugfs_create_file("ce_info", 0600, root_dentry, plat_priv, &cnss_ce_reg_debug_fops); #if !defined(CONFIG_CNSS2_KERNEL_5_15) debugfs_create_file("pci_switch_link", 0600, root_dentry, plat_priv, &cnss_pci_switch_link_fops); #endif return 0; } int cnss_debugfs_create(struct cnss_plat_data *plat_priv) { int ret = 0; struct dentry *root_dentry = NULL; if (!cnss_root_dentry) { cnss_root_dentry = debugfs_create_dir("cnss", 0); if (IS_ERR(cnss_root_dentry)) { ret = PTR_ERR(cnss_root_dentry); cnss_pr_err("Unable to create debugfs %d\n", ret); goto out; } /* Create qmi_record under /sys/kernel/debug/cnss2/ */ debugfs_create_file("qmi_record", 0600, cnss_root_dentry, NULL, &cnss_qmi_record_debug_fops); debugfs_create_file("mlo_config", 0600, cnss_root_dentry, NULL, &cnss_mlo_config_debug_fops); } root_dentry = debugfs_create_dir((char *)&plat_priv->device_name, cnss_root_dentry); if (IS_ERR(root_dentry)) { ret = PTR_ERR(root_dentry); cnss_pr_err("Unable to create debugfs %d\n", ret); goto out; } plat_priv->root_dentry = root_dentry; debugfs_create_file("pin_connect_result", 0644, root_dentry, plat_priv, &cnss_pin_connect_fops); debugfs_create_file("stats", 0644, root_dentry, plat_priv, &cnss_stats_fops); #if 1 debugfs_create_file("cpr_fuse", 0600, root_dentry, plat_priv, &cnss_cpr_fuse_debug_fops); #endif cnss_create_debug_only_node(plat_priv); out: return ret; } void cnss_debugfs_destroy(struct cnss_plat_data *plat_priv) { if (cnss_root_dentry) { debugfs_remove_recursive(cnss_root_dentry); cnss_root_dentry = NULL; } plat_priv->root_dentry = NULL; } #ifdef CONFIG_CNSS2_IPC_LOGGING int cnss_debug_init(void) { struct cnss_plat_data *plat_priv = NULL; #if 1 check_and_create_debugfs(); #endif cnss_ipc_log_context = ipc_log_context_create(CNSS_IPC_LOG_PAGES, "cnss", 0); if (!cnss_ipc_log_context) { cnss_pr_info("IPC Logging is disabled!\n"); return -EINVAL; } cnss_ipc_log_long_context = ipc_log_context_create(CNSS_IPC_LOG_PAGES, "cnss-long", 0); if (!cnss_ipc_log_long_context) { cnss_pr_info("IPC long logging is disabled!\n"); ipc_log_context_destroy(cnss_ipc_log_context); return -EINVAL; } return 0; } void cnss_debug_deinit(void) { if (cnss_ipc_log_long_context) { ipc_log_context_destroy(cnss_ipc_log_long_context); cnss_ipc_log_long_context = NULL; } if (cnss_ipc_log_context) { ipc_log_context_destroy(cnss_ipc_log_context); cnss_ipc_log_context = NULL; } } #endif bool cnss_wait_for_rddm_complete(struct cnss_plat_data *plat_priv) { int count = 0; if (!plat_priv) return true; if (test_bit(CNSS_RDDM_DUMP_IN_PROGRESS, &plat_priv->driver_state)) { cnss_pr_dbg("Waiting for RDDM collection for device 0x%lx\n", plat_priv->device_id); while (test_bit(CNSS_RDDM_DUMP_IN_PROGRESS, &plat_priv->driver_state)) { msleep(RDDM_DONE_DELAY); if (count++ > rddm_done_timeout * 10) { cnss_pr_err("RDDM collection timed-out %d seconds\n", rddm_done_timeout); CNSS_ASSERT(0); } } cnss_pr_dbg("RDDM collection wait ended for device 0x%lx\n", plat_priv->device_id); } return true; }