/**************************************************************************** * * Copyright (c) 2014 - 2016 Samsung Electronics Co., Ltd. All rights reserved * ****************************************************************************/ #include #include #include #include #include #include #include #include #include "mxman.h" /* Special case service driver that looks inside mxman */ #ifdef CONFIG_SCSC_FM_TEST #include "mx250_fm_test.h" #endif struct scsc_mx_fm_client { /* scsc_service_client has to be the first */ struct scsc_service_client fm_service_client; struct scsc_service *fm_service; struct scsc_mx *mx; bool fm_api_available; scsc_mifram_ref ref; struct workqueue_struct *fm_client_wq; struct work_struct fm_client_work; struct completion fm_client_work_completion; int fm_client_work_completion_status; bool ldo_on; }; static struct scsc_mx_fm_client *fm_client; /* service to start */ static int service_id = SCSC_SERVICE_ID_FM; static DEFINE_MUTEX(ss_lock); static u8 fm_client_failure_notification(struct scsc_service_client *client, struct mx_syserr_decode *err) { (void)client; SCSC_TAG_DEBUG(FM, "OK\n"); return err->level; } static bool fm_client_stop_on_failure(struct scsc_service_client *client, struct mx_syserr_decode *err) { (void)client; (void)err; mutex_lock(&ss_lock); fm_client->fm_api_available = false; mutex_unlock(&ss_lock); SCSC_TAG_DEBUG(FM, "OK\n"); return false; } static void fm_client_failure_reset(struct scsc_service_client *client, u8 level, u16 scsc_syserr_code) { (void)client; (void)level; (void)scsc_syserr_code; SCSC_TAG_DEBUG(FM, "OK\n"); } static int stop_close_service(void) { int r; if (!fm_client->fm_service) { SCSC_TAG_ERR(FM, "No fm_service\n"); r = -EINVAL; goto done; } r = scsc_mx_service_stop(fm_client->fm_service); if (r) { SCSC_TAG_ERR(FM, "scsc_mx_service_stop(fm_service) failed %d\n", r); goto done; } SCSC_TAG_DEBUG(FM, "scsc_mx_service_stop(fm_service) OK\n"); scsc_mx_service_mifram_free(fm_client->fm_service, fm_client->ref); r = scsc_mx_service_close(fm_client->fm_service); if (r) { SCSC_TAG_ERR(FM, "scsc_mx_service_close(fm_service) failed %d\n", r); goto done; } else SCSC_TAG_DEBUG(FM, "scsc_mx_service_close(fm_service) OK\n"); fm_client->fm_service = NULL; fm_client->ref = 0; done: return r; } static int open_start_service(void) { struct scsc_service *fm_service; int r; int r2; struct fm_ldo_conf *ldo_conf; scsc_mifram_ref ref; fm_service = scsc_mx_service_open(fm_client->mx, service_id, &fm_client->fm_service_client, &r); if (!fm_service) { r = -EINVAL; SCSC_TAG_ERR(FM, "scsc_mx_service_open(fm_service) failed %d\n", r); goto done; } /* Allocate memory */ r = scsc_mx_service_mifram_alloc(fm_service, sizeof(struct fm_ldo_conf), &ref, 32); if (r) { SCSC_TAG_ERR(FM, "scsc_mx_service_mifram_alloc(fm_service) failed %d\n", r); r2 = scsc_mx_service_close(fm_service); if (r2) SCSC_TAG_ERR(FM, "scsc_mx_service_close(fm_service) failed %d\n", r2); goto done; } ldo_conf = (struct fm_ldo_conf *)scsc_mx_service_mif_addr_to_ptr(fm_service, ref); ldo_conf->version = FM_LDO_CONFIG_VERSION; ldo_conf->ldo_on = fm_client->ldo_on; r = scsc_mx_service_start(fm_service, ref); if (r) { SCSC_TAG_ERR(FM, "scsc_mx_service_start(fm_service) failed %d\n", r); r2 = scsc_mx_service_close(fm_service); if (r2) SCSC_TAG_ERR(FM, "scsc_mx_service_close(fm_service) failed %d\n", r2); scsc_mx_service_mifram_free(fm_service, ref); goto done; } fm_client->fm_service = fm_service; fm_client->ref = ref; done: return r; } static int open_start_close_service(void) { int r; r = open_start_service(); if (r) { SCSC_TAG_ERR(FM, "Error starting service: open_start_service(fm_service) failed %d\n", r); if (!fm_client->ldo_on) { /* Do not return here. For the case where WLBT FW is crashed, and FM off request is * rejected, it's safest to continue to let scsc_service_on_halt_ldos_off() reset * the global flag to indicate that FM is no longer needed when WLBT next boots. * Otherwise LDO could be stuck always-on. */ } else return r; } if (fm_client->ldo_on) { /* FM turning on */ mxman_fm_on_halt_ldos_on(); } else { /* FM turning off */ mxman_fm_on_halt_ldos_off(); /* Invalidate stored FM params */ mxman_fm_set_params(NULL); } r = stop_close_service(); if (r) { SCSC_TAG_ERR(FM, "Error starting service: stop_close_service(fm_service) failed %d\n", r); return r; } return 0; } static void fm_client_work_func(struct work_struct *work) { SCSC_TAG_DEBUG(FM, "mx250: %s\n", __func__); fm_client->fm_client_work_completion_status = open_start_close_service(); if (fm_client->fm_client_work_completion_status) { SCSC_TAG_ERR(FM, "open_start_close_service(fm_service) failed %d\n", fm_client->fm_client_work_completion_status); } else { SCSC_TAG_DEBUG(FM, "OK\n"); } complete(&fm_client->fm_client_work_completion); } static void fm_client_wq_init(void) { fm_client->fm_client_wq = create_singlethread_workqueue("fm_client_wq"); INIT_WORK(&fm_client->fm_client_work, fm_client_work_func); } static void fm_client_wq_stop(void) { cancel_work_sync(&fm_client->fm_client_work); flush_workqueue(fm_client->fm_client_wq); } static void fm_client_wq_deinit(void) { fm_client_wq_stop(); destroy_workqueue(fm_client->fm_client_wq); } static void fm_client_wq_start(void) { queue_work(fm_client->fm_client_wq, &fm_client->fm_client_work); } static int fm_client_wq_start_blocking(void) { SCSC_TAG_DEBUG(FM, "mx250: %s\n", __func__); fm_client_wq_start(); wait_for_completion(&fm_client->fm_client_work_completion); if (fm_client->fm_client_work_completion_status) { SCSC_TAG_ERR(FM, "%s failed: fm_client_wq_completion_status = %d\n", __func__, fm_client->fm_client_work_completion_status); return fm_client->fm_client_work_completion_status; } SCSC_TAG_DEBUG(FM, "OK\n"); return 0; } static int mx250_fm_re(bool ldo_on) { int r; mutex_lock(&ss_lock); SCSC_TAG_DEBUG(FM, "mx250: %s\n", __func__); if (!fm_client) { SCSC_TAG_ERR(FM, "fm_client = NULL\n"); mutex_unlock(&ss_lock); return -ENODEV; } if (!fm_client->fm_api_available) { SCSC_TAG_WARNING(FM, "FM LDO API unavailable\n"); mutex_unlock(&ss_lock); return -EAGAIN; } fm_client->ldo_on = ldo_on; reinit_completion(&fm_client->fm_client_work_completion); r = fm_client_wq_start_blocking(); mutex_unlock(&ss_lock); return r; } /* * FM Radio is starting, tell WLBT drivers */ int mx250_fm_request(void) { SCSC_TAG_INFO(FM, "request\n"); return mx250_fm_re(true); } EXPORT_SYMBOL(mx250_fm_request); /* * FM Radio is stopping, tell WLBT drivers */ int mx250_fm_release(void) { SCSC_TAG_INFO(FM, "release\n"); return mx250_fm_re(false); } EXPORT_SYMBOL(mx250_fm_release); /* * FM Radio parameters are changing, tell WLBT drivers */ void mx250_fm_set_params(struct wlbt_fm_params *info) { SCSC_TAG_DEBUG(FM, "mx250: %s\n", __func__); if (!info) return; mutex_lock(&ss_lock); SCSC_TAG_INFO(FM, "freq %u\n", info->freq); mxman_fm_set_params(info); mutex_unlock(&ss_lock); } EXPORT_SYMBOL(mx250_fm_set_params); void fm_client_module_probe(struct scsc_mx_module_client *module_client, struct scsc_mx *mx, enum scsc_module_client_reason reason) { /* Avoid unused error */ (void)module_client; SCSC_TAG_INFO(FM, "probe\n"); mutex_lock(&ss_lock); if (reason == SCSC_MODULE_CLIENT_REASON_HW_PROBE) { fm_client = kzalloc(sizeof(*fm_client), GFP_KERNEL); if (!fm_client) { mutex_unlock(&ss_lock); return; } init_completion(&fm_client->fm_client_work_completion); fm_client_wq_init(); fm_client->fm_service_client.failure_notification = fm_client_failure_notification; fm_client->fm_service_client.stop_on_failure_v2 = fm_client_stop_on_failure; fm_client->fm_service_client.failure_reset_v2 = fm_client_failure_reset; fm_client->mx = mx; } fm_client->fm_api_available = true; SCSC_TAG_DEBUG(FM, "OK\n"); mutex_unlock(&ss_lock); } void fm_client_module_remove(struct scsc_mx_module_client *module_client, struct scsc_mx *mx, enum scsc_module_client_reason reason) { /* Avoid unused error */ (void)module_client; SCSC_TAG_INFO(FM, "remove\n"); mutex_lock(&ss_lock); if (reason == SCSC_MODULE_CLIENT_REASON_HW_REMOVE) { if (!fm_client) { mutex_unlock(&ss_lock); return; } if (fm_client->mx != mx) { SCSC_TAG_ERR(FM, "fm_client->mx != mx\n"); mutex_unlock(&ss_lock); return; } fm_client_wq_deinit(); kfree(fm_client); fm_client = NULL; } SCSC_TAG_DEBUG(FM, "OK\n"); mutex_unlock(&ss_lock); } /* FM client driver registration */ struct scsc_mx_module_client fm_client_driver = { .name = "FM client driver", .probe = fm_client_module_probe, .remove = fm_client_module_remove, }; static int __init scsc_fm_client_module_init(void) { int r; SCSC_TAG_INFO(FM, "init\n"); r = scsc_mx_module_register_client_module(&fm_client_driver); if (r) { SCSC_TAG_ERR(FM, "scsc_mx_module_register_client_module failed: r=%d\n", r); return r; } #ifdef CONFIG_SCSC_FM_TEST mx250_fm_test_init(); #endif return 0; } static void __exit scsc_fm_client_module_exit(void) { SCSC_TAG_INFO(FM, "exit\n"); scsc_mx_module_unregister_client_module(&fm_client_driver); #ifdef CONFIG_SCSC_FM_TEST mx250_fm_test_exit(); #endif SCSC_TAG_DEBUG(FM, "exit\n"); } late_initcall(scsc_fm_client_module_init); module_exit(scsc_fm_client_module_exit); MODULE_DESCRIPTION("FM Client Driver"); MODULE_AUTHOR("SCSC"); MODULE_LICENSE("GPL");