From patchwork Tue Feb 4 23:18:30 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Fred Treven X-Patchwork-Id: 13960208 Received: from mx0b-001ae601.pphosted.com (mx0a-001ae601.pphosted.com [67.231.149.25]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 63FFE21C17B; Tue, 4 Feb 2025 23:22:03 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=67.231.149.25 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1738711325; cv=none; b=ki4bO6VcA1bfzjCNFYpr/QOvpNVh9M8i97v3VsJmPK8ciqVJTtf353aWsla2sw9qsJwNPxv3fs2wSjQ0MqeRfWyXmilRthfdc/GUm8iTv1/T8lEh1AAMrDLf7Kf8qfPO3qcl0S02yXGQs+ylHQb0saTWZ787OKPyqPz41GLKJGM= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1738711325; c=relaxed/simple; bh=e2TVzUbRqBCxTVqTlfUdnaIbDJGEbaayIa/i2ReZiNw=; h=From:To:CC:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=uSuH88KAzNajJLxUYkXJP+c136JrCAT4OvlbXNOQJWmp2954EzRP/JYfcIqEzC7rFLwQ8XBpydXnrNEoV7bRNaqFdJiOu2MTQQCJ4ywp/3IKQkp5/0IxMQ3LhJJbzSy1rPqlH+UtLazLeFAj98iL9QRuvjro/R6ucAiFBky54ro= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=opensource.cirrus.com; spf=pass smtp.mailfrom=opensource.cirrus.com; dkim=pass (2048-bit key) header.d=cirrus.com header.i=@cirrus.com header.b=qmATbGk+; arc=none smtp.client-ip=67.231.149.25 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=opensource.cirrus.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=opensource.cirrus.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=cirrus.com header.i=@cirrus.com header.b="qmATbGk+" Received: from pps.filterd (m0077473.ppops.net [127.0.0.1]) by mx0a-001ae601.pphosted.com (8.18.1.2/8.18.1.2) with ESMTP id 514FmWAQ029190; Tue, 4 Feb 2025 17:19:32 -0600 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cirrus.com; h=cc :content-transfer-encoding:content-type:date:from:in-reply-to :message-id:mime-version:references:subject:to; s= PODMain02222019; bh=iwVGm2zIZxv30QAxGxxXgudWn5/8W/GZ/AyWJMQKILI=; b= qmATbGk+dCT6TqCrWx5EByS1QmaxM3kGEU55NF+y9IWGYnwWQEMQs2tgFUcCs3yv C+tFM2hN5y7iGZ++PJH2W75G5CB15cKTXOfuXAFV6/hL57rFoZEWsA5SyxKusTJc qXbCkjXLgrMhqIcS18sOghm8ybFO9x6fXdENN/m/AgF2x+X2lUOXVScvmXUvW0QT ibznDUU8fwbFUSvNv9mzN+NkEgBxJK3BQnyzl5hukLb5q2BnBbhnMa20+BXmyYRg q5yP4TloRtDWsfYzApAi3H2he0ft+zLZ9lL+ihG3v0wv49b2qqQy75K7/THv4E6G 67XFNc5a8hD6wvXENWUR+A== Received: from ediex01.ad.cirrus.com ([84.19.233.68]) by mx0a-001ae601.pphosted.com (PPS) with ESMTPS id 44hhw53pk0-1 (version=TLSv1.2 cipher=ECDHE-RSA-AES256-GCM-SHA384 bits=256 verify=NOT); Tue, 04 Feb 2025 17:19:32 -0600 (CST) Received: from ediex01.ad.cirrus.com (198.61.84.80) by ediex01.ad.cirrus.com (198.61.84.80) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.2.1544.14; Tue, 4 Feb 2025 23:19:29 +0000 Received: from ediswmail9.ad.cirrus.com (198.61.86.93) by anon-ediex01.ad.cirrus.com (198.61.84.80) with Microsoft SMTP Server id 15.2.1544.14 via Frontend Transport; Tue, 4 Feb 2025 23:19:24 +0000 Received: from ftrev.crystal.cirrus.com (ftrev.ad.cirrus.com [141.131.145.81]) by ediswmail9.ad.cirrus.com (Postfix) with ESMTP id 7CF3F820248; Tue, 4 Feb 2025 23:19:20 +0000 (UTC) From: Fred Treven To: Lee Jones , Rob Herring , "Krzysztof Kozlowski" , Conor Dooley , "Simon Trimmer" , Charles Keepax , Richard Fitzgerald , Dmitry Torokhov , James Ogletree , Ben Bright , Liam Girdwood , Mark Brown , Jaroslav Kysela , Takashi Iwai , David Rhodes , Jeff LaBundy , Heiko Stuebner , Karel Balej , Igor Prusov , Jack Yu , Weidong Wang , Binbin Zhou , Prasad Kumpatla , "Paul Handrigan" , Masahiro Yamada , Nuno Sa , Fred Treven CC: , , , , , Subject: [PATCH RESEND 1/7] firmware: cs_dsp: Fix error checking in wseq_write() Date: Tue, 4 Feb 2025 17:18:30 -0600 Message-ID: <20250204231835.2000457-2-ftreven@opensource.cirrus.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20250204231835.2000457-1-ftreven@opensource.cirrus.com> References: <20250204231835.2000457-1-ftreven@opensource.cirrus.com> Precedence: bulk X-Mailing-List: linux-input@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Proofpoint-ORIG-GUID: LLWwZEzpZPzgeIlQ15q6_d9H5f4dIMCe X-Authority-Analysis: v=2.4 cv=W/3CVQWk c=1 sm=1 tr=0 ts=67a2a084 cx=c_pps a=uGhh+3tQvKmCLpEUO+DX4w==:117 a=uGhh+3tQvKmCLpEUO+DX4w==:17 a=T2h4t0Lz3GQA:10 a=w1d2syhTAAAA:8 a=b0R_zkI-s6yCh7IOb7YA:9 a=YXXWInSmI4Sqt1AkVdoW:22 X-Proofpoint-GUID: LLWwZEzpZPzgeIlQ15q6_d9H5f4dIMCe X-Proofpoint-Spam-Reason: safe cs_dsp_coeff_write_ctrl() may return a non-zero value (1) upon success. Change error checking in the write sequencer code such that it checks for negative errnos rather than any non-zero value when using this function. Signed-off-by: Fred Treven --- drivers/firmware/cirrus/cs_dsp.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/firmware/cirrus/cs_dsp.c b/drivers/firmware/cirrus/cs_dsp.c index 5365e9a43000..56315b0b5583 100644 --- a/drivers/firmware/cirrus/cs_dsp.c +++ b/drivers/firmware/cirrus/cs_dsp.c @@ -3702,7 +3702,7 @@ int cs_dsp_wseq_write(struct cs_dsp *dsp, struct cs_dsp_wseq *wseq, ret = cs_dsp_coeff_write_ctrl(wseq->ctl, op_end->offset / sizeof(u32), &op_end->data, sizeof(u32)); - if (ret) + if (ret < 0) goto op_new_free; list_add_tail(&op_new->list, &op_end->list); @@ -3710,7 +3710,7 @@ int cs_dsp_wseq_write(struct cs_dsp *dsp, struct cs_dsp_wseq *wseq, ret = cs_dsp_coeff_write_ctrl(wseq->ctl, op_new->offset / sizeof(u32), words, new_op_size); - if (ret) + if (ret < 0) goto op_new_free; return 0; From patchwork Tue Feb 4 23:18:31 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Fred Treven X-Patchwork-Id: 13960214 Received: from mx0b-001ae601.pphosted.com (mx0b-001ae601.pphosted.com [67.231.152.168]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 689932163BA; Tue, 4 Feb 2025 23:27:12 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=67.231.152.168 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1738711634; cv=none; b=j4q8SRJ+oXbCsGeexbFAGVYgcEI4To7Q/GV3RqaxwKU+d9BAUgkiyJxWfr3ApucnaYopaTDBK4n1HvZK/CIvkTeAmHHBpEVRgNZ5/HqfZZJAVm+1l3Y+WF6mqP1VNo3fv/L3o7ICQ5K7Mni7mcYH58nDFtsBdsCwySrBrW+/a70= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1738711634; c=relaxed/simple; bh=1Ndbk2BaIajsSq6LVjOKCXHTVo6NfBh3Ifl5W7zU9q4=; h=From:To:CC:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=UoV8VaXnPplX6tp8nCGnXl/X1LAjAvva+8dfW1FbC6ZQBGDJ6kqeiauRAWyFCxfCuER+T3VDi4FVxQsaIJGZc0XudmDk9LFBgknNrDKO3AKBk8tXyth7chYZiL42NOM4HCRIQ6zKv9FE6w0KvIalfcoeFnmMCj7HEN0nkVd8C04= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=opensource.cirrus.com; spf=pass smtp.mailfrom=opensource.cirrus.com; dkim=pass (2048-bit key) header.d=cirrus.com header.i=@cirrus.com header.b=fXsSkeXF; arc=none smtp.client-ip=67.231.152.168 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=opensource.cirrus.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=opensource.cirrus.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=cirrus.com header.i=@cirrus.com header.b="fXsSkeXF" Received: from pps.filterd (m0077474.ppops.net [127.0.0.1]) by mx0b-001ae601.pphosted.com (8.18.1.2/8.18.1.2) with ESMTP id 514Klioc016339; Tue, 4 Feb 2025 17:19:43 -0600 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cirrus.com; h=cc :content-transfer-encoding:content-type:date:from:in-reply-to :message-id:mime-version:references:subject:to; s= PODMain02222019; bh=4rk84s04nJmPDhNfVK0qQcCyoF1+d6Q1J1vzsSv4K4c=; b= fXsSkeXFyA3zFwkflPotTk/AvJWBN7rwXu9+spJWWZf3DFgttkG+ZQ16iGddrTJ2 c9A5rzMb7mh9d6Fg3Bozyr6W4IZ46E1tIuVgmtNOjfTXKLmBCihoYq0xg77UhqrQ cbL/8BZ0XOimg6VoGFExUUqYfuLjCpthTjksSa7T8+Lr4x39P2EsJlYVrZSDWe6C qhKlBajk1ZgrRO4JBMx40u1aQ4dRhjWXnUQH8tXW6eUugXZL7hWvQqUjQk0+2pup 5/sz7ahhHNe5KvSyXrA02UCrgP2L6El8Tozj3SfdWuKg/dJolP+3a8y+RU8nhVTJ 7Mzx1Yk1lWKPXnnPRLAXzw== Received: from ediex02.ad.cirrus.com ([84.19.233.68]) by mx0b-001ae601.pphosted.com (PPS) with ESMTPS id 44hgwm3spw-1 (version=TLSv1.2 cipher=ECDHE-RSA-AES256-GCM-SHA384 bits=256 verify=NOT); Tue, 04 Feb 2025 17:19:43 -0600 (CST) Received: from ediex01.ad.cirrus.com (198.61.84.80) by ediex02.ad.cirrus.com (198.61.84.81) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.2.1544.14; Tue, 4 Feb 2025 23:19:41 +0000 Received: from ediswmail9.ad.cirrus.com (198.61.86.93) by anon-ediex01.ad.cirrus.com (198.61.84.80) with Microsoft SMTP Server id 15.2.1544.14 via Frontend Transport; Tue, 4 Feb 2025 23:19:36 +0000 Received: from ftrev.crystal.cirrus.com (ftrev.ad.cirrus.com [141.131.145.81]) by ediswmail9.ad.cirrus.com (Postfix) with ESMTP id 5020F820248; Tue, 4 Feb 2025 23:19:32 +0000 (UTC) From: Fred Treven To: Lee Jones , Rob Herring , "Krzysztof Kozlowski" , Conor Dooley , "Simon Trimmer" , Charles Keepax , Richard Fitzgerald , Dmitry Torokhov , James Ogletree , Ben Bright , Liam Girdwood , Mark Brown , Jaroslav Kysela , Takashi Iwai , David Rhodes , Jeff LaBundy , Heiko Stuebner , Karel Balej , Igor Prusov , Jack Yu , Weidong Wang , Binbin Zhou , Prasad Kumpatla , "Paul Handrigan" , Masahiro Yamada , Nuno Sa , Fred Treven CC: , , , , , Subject: [PATCH RESEND 2/7] firmware: cs_dsp: Check for valid num_regs in cs_dsp_wseq_multi_write() Date: Tue, 4 Feb 2025 17:18:31 -0600 Message-ID: <20250204231835.2000457-3-ftreven@opensource.cirrus.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20250204231835.2000457-1-ftreven@opensource.cirrus.com> References: <20250204231835.2000457-1-ftreven@opensource.cirrus.com> Precedence: bulk X-Mailing-List: linux-input@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Authority-Analysis: v=2.4 cv=EPv800ZC c=1 sm=1 tr=0 ts=67a2a08f cx=c_pps a=uGhh+3tQvKmCLpEUO+DX4w==:117 a=uGhh+3tQvKmCLpEUO+DX4w==:17 a=T2h4t0Lz3GQA:10 a=w1d2syhTAAAA:8 a=pYzt1GAZzx6qoJfKPqoA:9 a=zZCYzV9kfG8A:10 a=YXXWInSmI4Sqt1AkVdoW:22 X-Proofpoint-ORIG-GUID: QwIhPN2wXpUWh2VjXVyow-eG4EN6tgsG X-Proofpoint-GUID: QwIhPN2wXpUWh2VjXVyow-eG4EN6tgsG X-Proofpoint-Spam-Reason: safe If a value of 0 or below is passed into cs_dsp_wseq_multi_write() the function will never enter its for loop. Verify that num_regs passed into the function is valid and throw a user-visible error if not. Signed-off-by: Fred Treven --- drivers/firmware/cirrus/cs_dsp.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/firmware/cirrus/cs_dsp.c b/drivers/firmware/cirrus/cs_dsp.c index 56315b0b5583..aacf6960d1ea 100644 --- a/drivers/firmware/cirrus/cs_dsp.c +++ b/drivers/firmware/cirrus/cs_dsp.c @@ -3743,6 +3743,11 @@ int cs_dsp_wseq_multi_write(struct cs_dsp *dsp, struct cs_dsp_wseq *wseq, { int i, ret; + if (num_regs <= 0) { + cs_dsp_err(dsp, "Invalid number of regs: %d\n", num_regs); + return -EINVAL; + } + for (i = 0; i < num_regs; i++) { ret = cs_dsp_wseq_write(dsp, wseq, reg_seq[i].reg, reg_seq[i].def, op_code, update); From patchwork Tue Feb 4 23:18:32 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Fred Treven X-Patchwork-Id: 13960213 Received: from mx0b-001ae601.pphosted.com (mx0b-001ae601.pphosted.com [67.231.152.168]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 9C9162163BA; Tue, 4 Feb 2025 23:25:33 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=67.231.152.168 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1738711535; cv=none; b=ffOu+WCEFC4YukALYGY1O5zmOOIJlIkLIDtD/leAuiHj3IVj4AWZOFwF5WjylVNvGz+gqG74J04IONCoI682bIUXtvC/YR2Wo9o4WPSg9QGn6OKnkSHRd8JSx40aJdBSvAfyxc5usTeSYTM69rm2DTcZoD0HxZ/UaDz2kZhI8jY= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1738711535; c=relaxed/simple; bh=ww5lwq80YlcASZ3YG6eNRBG2H6zs9+reQUjR4d3hVtM=; h=From:To:CC:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=bzEPiRXt45iFTHG6mb/dIayAW37Ncl9L8cZsM5LypyUtrdXJ+w070PutPdPmToDxRvVYU2f9UsEtk8xzgRURMBWqD9PiA0Q9gzvTwxllRDTqIS9lfaH+CopaQdrZBvCmIN7TO3n7JPA/ahqznXptPihp41CmyzqtKzsngIeVwsE= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=opensource.cirrus.com; spf=pass smtp.mailfrom=opensource.cirrus.com; dkim=pass (2048-bit key) header.d=cirrus.com header.i=@cirrus.com header.b=U/Lbx5t5; arc=none smtp.client-ip=67.231.152.168 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=opensource.cirrus.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=opensource.cirrus.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=cirrus.com header.i=@cirrus.com header.b="U/Lbx5t5" Received: from pps.filterd (m0077474.ppops.net [127.0.0.1]) by mx0b-001ae601.pphosted.com (8.18.1.2/8.18.1.2) with ESMTP id 514FmAYt022131; Tue, 4 Feb 2025 17:19:53 -0600 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cirrus.com; h=cc :content-transfer-encoding:content-type:date:from:in-reply-to :message-id:mime-version:references:subject:to; s= PODMain02222019; bh=yTyJHpwhqETQXxe7Ynl0sgZnYO0w/sB7CjGflqNdOyw=; b= U/Lbx5t5Tx/HlP7OXVyXJcfxtft5pkyuAMybAwYClkug1tXsvQgIU+//F9yNGxuq hnsWvbLXwt1DYW2LfhkMCTH6vBBuqUAP9/XFHQQPDcpFcWFx3mY9xMBEL6LN2Nso 2/SCqQOmfsfWr+DAyFuWSgaICXS5x/ZkHbOMpS8HP8twNEPwBx1fqyfk2iyteOYF cuNhrKIJ1JEUQKHVIb6cxrpa5jZ4wa9FJllsCCo58fX3NCAKF50E/xRxVH3VEBxv uYcbDCSYblqo8NgKCiHEmxC85T8Df0CVMYew4JHZOy2SaXamj5U3NRLOG4k4tUKk 4a+n9K+JlfR5yibN0LT8yQ== Received: from ediex01.ad.cirrus.com ([84.19.233.68]) by mx0b-001ae601.pphosted.com (PPS) with ESMTPS id 44hgwm3sq1-1 (version=TLSv1.2 cipher=ECDHE-RSA-AES256-GCM-SHA384 bits=256 verify=NOT); Tue, 04 Feb 2025 17:19:52 -0600 (CST) Received: from ediex02.ad.cirrus.com (198.61.84.81) by ediex01.ad.cirrus.com (198.61.84.80) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.2.1544.14; Tue, 4 Feb 2025 23:19:51 +0000 Received: from ediswmail9.ad.cirrus.com (198.61.86.93) by anon-ediex02.ad.cirrus.com (198.61.84.81) with Microsoft SMTP Server id 15.2.1544.14 via Frontend Transport; Tue, 4 Feb 2025 23:19:46 +0000 Received: from ftrev.crystal.cirrus.com (ftrev.ad.cirrus.com [141.131.145.81]) by ediswmail9.ad.cirrus.com (Postfix) with ESMTP id BA64D820270; Tue, 4 Feb 2025 23:19:41 +0000 (UTC) From: Fred Treven To: Lee Jones , Rob Herring , "Krzysztof Kozlowski" , Conor Dooley , "Simon Trimmer" , Charles Keepax , Richard Fitzgerald , Dmitry Torokhov , James Ogletree , Ben Bright , Liam Girdwood , Mark Brown , Jaroslav Kysela , Takashi Iwai , David Rhodes , Jeff LaBundy , Heiko Stuebner , Karel Balej , Igor Prusov , Jack Yu , Weidong Wang , Binbin Zhou , Prasad Kumpatla , "Paul Handrigan" , Masahiro Yamada , Nuno Sa , Fred Treven CC: , , , , , Subject: [PATCH RESEND 3/7] firmware: cs_dsp: Add ability to load multiple coefficient files Date: Tue, 4 Feb 2025 17:18:32 -0600 Message-ID: <20250204231835.2000457-4-ftreven@opensource.cirrus.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20250204231835.2000457-1-ftreven@opensource.cirrus.com> References: <20250204231835.2000457-1-ftreven@opensource.cirrus.com> Precedence: bulk X-Mailing-List: linux-input@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Authority-Analysis: v=2.4 cv=EPv800ZC c=1 sm=1 tr=0 ts=67a2a098 cx=c_pps a=uGhh+3tQvKmCLpEUO+DX4w==:117 a=uGhh+3tQvKmCLpEUO+DX4w==:17 a=T2h4t0Lz3GQA:10 a=w1d2syhTAAAA:8 a=MbgxawBnhQrWfd7Trs8A:9 a=YXXWInSmI4Sqt1AkVdoW:22 X-Proofpoint-ORIG-GUID: 9_dhfxmiASLxP9Y926m-CR9JRVBGU0yo X-Proofpoint-GUID: 9_dhfxmiASLxP9Y926m-CR9JRVBGU0yo X-Proofpoint-Spam-Reason: safe Add cs_dsp_power_up_multiple() which accepts an array of cs_dsp_coeff_desc firmware-filename pairs to load. This enables the user to load more than one tuning file along with the associated firmware. Change cs_dsp_power_up() to make use of the new function with a single coefficient file. Signed-off-by: Fred Treven --- drivers/firmware/cirrus/cs_dsp.c | 61 ++++++++++++++++++++------ include/linux/firmware/cirrus/cs_dsp.h | 14 ++++++ 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/drivers/firmware/cirrus/cs_dsp.c b/drivers/firmware/cirrus/cs_dsp.c index aacf6960d1ea..68563186637e 100644 --- a/drivers/firmware/cirrus/cs_dsp.c +++ b/drivers/firmware/cirrus/cs_dsp.c @@ -2695,28 +2695,29 @@ static void cs_dsp_halo_stop_watchdog(struct cs_dsp *dsp) } /** - * cs_dsp_power_up() - Downloads firmware to the DSP - * @dsp: pointer to DSP structure + * cs_dsp_power_up_multiple() - Downloads firmware and multiple coefficient files to the DSP + * @dsp: pointer to the DSP structure * @wmfw_firmware: the firmware to be sent * @wmfw_filename: file name of firmware to be sent - * @coeff_firmware: the coefficient data to be sent - * @coeff_filename: file name of coefficient to data be sent + * @coeffs: coefficient data and filename pairs to be sent + * @num_coeffs: number of coefficient files to be sent * @fw_name: the user-friendly firmware name * * This function is used on ADSP2 and Halo DSP cores, it powers-up the DSP core * and downloads the firmware but does not start the firmware running. The * cs_dsp booted flag will be set once completed and if the core has a low-power * memory retention mode it will be put into this state after the firmware is - * downloaded. + * downloaded. Differs from cs_dsp_power_up() in that it allows for multiple + * coefficient files to be downloaded. * * Return: Zero for success, a negative number on error. */ -int cs_dsp_power_up(struct cs_dsp *dsp, - const struct firmware *wmfw_firmware, const char *wmfw_filename, - const struct firmware *coeff_firmware, const char *coeff_filename, - const char *fw_name) +int cs_dsp_power_up_multiple(struct cs_dsp *dsp, + const struct firmware *wmfw_firmware, const char *wmfw_filename, + struct cs_dsp_coeff_desc *coeffs, int num_coeffs, + const char *fw_name) { - int ret; + int i, ret; mutex_lock(&dsp->pwr_lock); @@ -2742,9 +2743,12 @@ int cs_dsp_power_up(struct cs_dsp *dsp, if (ret != 0) goto err_ena; - ret = cs_dsp_load_coeff(dsp, coeff_firmware, coeff_filename); - if (ret != 0) - goto err_ena; + for (i = 0; i < num_coeffs; i++) { + ret = cs_dsp_load_coeff(dsp, coeffs[i].coeff_firmware, + coeffs[i].coeff_filename); + if (ret != 0) + goto err_ena; + } /* Initialize caches for enabled and unset controls */ ret = cs_dsp_coeff_init_control_caches(dsp); @@ -2770,6 +2774,37 @@ int cs_dsp_power_up(struct cs_dsp *dsp, return ret; } +EXPORT_SYMBOL_NS_GPL(cs_dsp_power_up_multiple, "FW_CS_DSP"); + +/** + * cs_dsp_power_up() - Downloads firmware to the DSP + * @dsp: pointer to DSP structure + * @wmfw_firmware: the firmware to be sent + * @wmfw_filename: file name of firmware to be sent + * @coeff_firmware: the coefficient data to be sent + * @coeff_filename: file name of coefficient to data be sent + * @fw_name: the user-friendly firmware name + * + * This function is used on ADSP2 and Halo DSP cores, it powers-up the DSP core + * and downloads the firmware but does not start the firmware running. The + * cs_dsp booted flag will be set once completed and if the core has a low-power + * memory retention mode it will be put into this state after the firmware is + * downloaded. + * + * Return: Zero for success, a negative number on error. + */ +int cs_dsp_power_up(struct cs_dsp *dsp, + const struct firmware *wmfw_firmware, const char *wmfw_filename, + const struct firmware *coeff_firmware, const char *coeff_filename, + const char *fw_name) +{ + struct cs_dsp_coeff_desc coeff_desc; + + coeff_desc.coeff_firmware = coeff_firmware; + coeff_desc.coeff_filename = coeff_filename; + + return cs_dsp_power_up_multiple(dsp, wmfw_firmware, wmfw_filename, &coeff_desc, 1, fw_name); +} EXPORT_SYMBOL_NS_GPL(cs_dsp_power_up, "FW_CS_DSP"); /** diff --git a/include/linux/firmware/cirrus/cs_dsp.h b/include/linux/firmware/cirrus/cs_dsp.h index 7cae703b3137..4c4e746be6fa 100644 --- a/include/linux/firmware/cirrus/cs_dsp.h +++ b/include/linux/firmware/cirrus/cs_dsp.h @@ -52,6 +52,16 @@ #define CS_DSP_WSEQ_UNLOCK 0xFD #define CS_DSP_WSEQ_END 0xFF +/** + * struct cs_dsp_coeff_desc - Describes a coeff. file + filename pair + * @coeff_firmware: Firmware struct to populate with coeff. data + * @coeff_filename: File from which coeff. data is loaded + */ +struct cs_dsp_coeff_desc { + const struct firmware *coeff_firmware; + const char *coeff_filename; +}; + /** * struct cs_dsp_region - Describes a logical memory region in DSP address space * @type: Memory region type @@ -227,6 +237,10 @@ int cs_dsp_adsp1_power_up(struct cs_dsp *dsp, const struct firmware *coeff_firmware, const char *coeff_filename, const char *fw_name); void cs_dsp_adsp1_power_down(struct cs_dsp *dsp); +int cs_dsp_power_up_multiple(struct cs_dsp *dsp, + const struct firmware *wmfw_firmware, const char *wmfw_filename, + struct cs_dsp_coeff_desc *coeffs, int num_coeffs, + const char *fw_name); int cs_dsp_power_up(struct cs_dsp *dsp, const struct firmware *wmfw_firmware, const char *wmfw_filename, const struct firmware *coeff_firmware, const char *coeff_filename, From patchwork Tue Feb 4 23:18:33 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Fred Treven X-Patchwork-Id: 13960205 Received: from mx0b-001ae601.pphosted.com (mx0a-001ae601.pphosted.com [67.231.149.25]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 6580B21ADA3; Tue, 4 Feb 2025 23:20:46 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=67.231.149.25 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1738711249; cv=none; b=rVHwgC0k9p2ARce36xh0tH3kZluwKsXlRRMWhe60yx3+hcMFLb7W113CQY0jyovzg1Hax4oRXE/DHmTHUqUi3unHN50WXp0pWqXpU909eCvTFQpEEmuTjFbn9dRMbTjrH8noWENYmxt/Iy8ieiGheZvTxnq1OxHEy5U3+1Cg5qw= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1738711249; c=relaxed/simple; bh=PQC6lO+NQ+lEKD+JRu90LE3Nqd7VmbhE6PsaGMSNg4M=; h=From:To:CC:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=lXC8WvpZaYKSE7oj5aQpxL2o74WuUpTamQ3OFofZoUgrMi3lnl5lfWZLEcN/wbWENitGEwuXtlwHaz/wuC9l4SbsDbioTSoRJzq9seMtneOipWA17LzOX1ESKOMRro/rYuSJ/oQfDK2gGz0WPlsSoco8O4LYp89YKL9qjyYPSnk= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=opensource.cirrus.com; spf=pass smtp.mailfrom=opensource.cirrus.com; dkim=pass (2048-bit key) header.d=cirrus.com header.i=@cirrus.com header.b=b7MRZ2nj; arc=none smtp.client-ip=67.231.149.25 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=opensource.cirrus.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=opensource.cirrus.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=cirrus.com header.i=@cirrus.com header.b="b7MRZ2nj" Received: from pps.filterd (m0077473.ppops.net [127.0.0.1]) by mx0a-001ae601.pphosted.com (8.18.1.2/8.18.1.2) with ESMTP id 514FmiGl029318; Tue, 4 Feb 2025 17:20:10 -0600 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cirrus.com; h=cc :content-transfer-encoding:content-type:date:from:in-reply-to :message-id:mime-version:references:subject:to; s= PODMain02222019; bh=BCT2AIKVtExqG27DTd5hLqvs/DalIumwuqwNjQzI47U=; b= b7MRZ2njGYPT5t4l1Rd7YXY+LjewjwU9/fQ8Q3lhQqprVHPKAh5fpIgf0Ve7tUrO rSEiTl2VA4doV/iEdR0Vad4DbNtnNc5CxPjr1LiIU07A/FWmWNbeJYhWfOVtLeHx TFVF8nkU66KWxN3PY5PD675w3canXdpuSkIHnpXfK0ClPldVIUAZuZD9diwjotO2 kFrVp7KQdh0K+jJzy1TTEEhw9ItvtBWU3rxoUSAOaxYpD7M1SRqeolYpvWvPoIO2 vXj58XT11AD62xz4YlIK8Na3CD9m6o5rlLU14HTrJZ8SUp0fAPL+xyvg3FnILaCs kark0GrbJ8qnTDGeZAF+Ag== Received: from ediex01.ad.cirrus.com ([84.19.233.68]) by mx0a-001ae601.pphosted.com (PPS) with ESMTPS id 44hhw53pkg-1 (version=TLSv1.2 cipher=ECDHE-RSA-AES256-GCM-SHA384 bits=256 verify=NOT); Tue, 04 Feb 2025 17:20:10 -0600 (CST) Received: from ediex02.ad.cirrus.com (198.61.84.81) by ediex01.ad.cirrus.com (198.61.84.80) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.2.1544.14; Tue, 4 Feb 2025 23:20:08 +0000 Received: from ediswmail9.ad.cirrus.com (198.61.86.93) by anon-ediex02.ad.cirrus.com (198.61.84.81) with Microsoft SMTP Server id 15.2.1544.14 via Frontend Transport; Tue, 4 Feb 2025 23:20:03 +0000 Received: from ftrev.crystal.cirrus.com (ftrev.ad.cirrus.com [141.131.145.81]) by ediswmail9.ad.cirrus.com (Postfix) with ESMTP id B0AFF820248; Tue, 4 Feb 2025 23:19:58 +0000 (UTC) From: Fred Treven To: Lee Jones , Rob Herring , "Krzysztof Kozlowski" , Conor Dooley , "Simon Trimmer" , Charles Keepax , Richard Fitzgerald , Dmitry Torokhov , James Ogletree , Ben Bright , Liam Girdwood , Mark Brown , Jaroslav Kysela , Takashi Iwai , David Rhodes , Jeff LaBundy , Heiko Stuebner , Karel Balej , Igor Prusov , Jack Yu , Weidong Wang , Binbin Zhou , Prasad Kumpatla , "Paul Handrigan" , Masahiro Yamada , Nuno Sa , Fred Treven CC: , , , , , Subject: [PATCH RESEND 4/7] dt-bindings: mfd: cirrus,cs40l26: Support for CS40L26 Date: Tue, 4 Feb 2025 17:18:33 -0600 Message-ID: <20250204231835.2000457-5-ftreven@opensource.cirrus.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20250204231835.2000457-1-ftreven@opensource.cirrus.com> References: <20250204231835.2000457-1-ftreven@opensource.cirrus.com> Precedence: bulk X-Mailing-List: linux-input@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Proofpoint-ORIG-GUID: Wil3jMa_PyzzZx9lUo4ysaKtu89G_laj X-Authority-Analysis: v=2.4 cv=W/3CVQWk c=1 sm=1 tr=0 ts=67a2a0aa cx=c_pps a=uGhh+3tQvKmCLpEUO+DX4w==:117 a=uGhh+3tQvKmCLpEUO+DX4w==:17 a=T2h4t0Lz3GQA:10 a=gEfo2CItAAAA:8 a=w1d2syhTAAAA:8 a=wuh2pNzbNvaVoUcC6a8A:9 a=sptkURWiP4Gy88Gu7hUp:22 a=YXXWInSmI4Sqt1AkVdoW:22 X-Proofpoint-GUID: Wil3jMa_PyzzZx9lUo4ysaKtu89G_laj X-Proofpoint-Spam-Reason: safe Introduce required basic devicetree parameters for the initial commit of CS40L26. Signed-off-by: Fred Treven --- .../bindings/mfd/cirrus,cs40l26.yaml | 81 +++++++++++++++++++ MAINTAINERS | 4 +- 2 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 Documentation/devicetree/bindings/mfd/cirrus,cs40l26.yaml diff --git a/Documentation/devicetree/bindings/mfd/cirrus,cs40l26.yaml b/Documentation/devicetree/bindings/mfd/cirrus,cs40l26.yaml new file mode 100644 index 000000000000..a3cccb1a2d92 --- /dev/null +++ b/Documentation/devicetree/bindings/mfd/cirrus,cs40l26.yaml @@ -0,0 +1,81 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/mfd/cirrus,cs40l26.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Cirrus Logic CS40L26 Boosted Haptic Amplifier + +maintainers: + - Fred Treven + - patches@opensource.cirrus.com + +description: + CS40L26 is a Boosted Haptic Driver with Integrated DSP, Waveform Memory, + Advanced Closed Loop Algorithms, and LRA protection + +properties: + compatible: + enum: + - cirrus,cs40l26a + - cirrus,cs40l27b + + reg: + maxItems: 1 + + interrupts: + maxItems: 1 + + reset-gpios: + maxItems: 1 + + va-supply: + description: Regulator for VA analog voltage + + vp-supply: + description: Regulator for VP voltage + + cirrus,bst-ipk-microamp: + description: + Maximum current that can be drawn by the device's boost converter. + multipleOf: 50000 + minimum: 1600000 + maximum: 4800000 + default: 4500000 + + cirrus,bst-ctl-microvolt: + description: Maximum target voltage to which DSP may increase the VBST supply. + multipleOf: 50000 + minimum: 2550000 + maximum: 11000000 + default: 11000000 + +required: + - compatible + - reg + - interrupts + - reset-gpios + +additionalProperties: false + +examples: + - | + #include + #include + + i2c { + #address-cells = <1>; + #size-cells = <0>; + + haptic-driver@58 { + compatible = "cirrus,cs40l26a"; + reg = <0x58>; + interrupt-parent = <&gpio>; + interrupts = <57 IRQ_TYPE_LEVEL_LOW>; + reset-gpios = <&gpio 54 GPIO_ACTIVE_LOW>; + va-supply = <&vreg>; + vp-supply = <&vreg>; + cirrus,bst-ctl-microvolt = <2600000>; + cirrus,bst-ipk-microamp = <1650000>; + }; + }; diff --git a/MAINTAINERS b/MAINTAINERS index bc8ce7af3303..9c4105bf0a32 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -5546,11 +5546,11 @@ F: sound/soc/codecs/cs* CIRRUS LOGIC HAPTIC DRIVERS M: James Ogletree -M: Fred Treven +M: Fred Treven M: Ben Bright L: patches@opensource.cirrus.com S: Supported -F: Documentation/devicetree/bindings/input/cirrus,cs40l50.yaml +F: Documentation/devicetree/bindings/input/cirrus,cs40l* F: drivers/input/misc/cs40l* F: drivers/mfd/cs40l* F: include/linux/mfd/cs40l* From patchwork Tue Feb 4 23:18:34 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Fred Treven X-Patchwork-Id: 13960206 Received: from mx0b-001ae601.pphosted.com (mx0a-001ae601.pphosted.com [67.231.149.25]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id CCC2A21CA17; Tue, 4 Feb 2025 23:20:54 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=67.231.149.25 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1738711258; cv=none; b=FUtuX1TPLy2S7uhuhkjxu9V8ICbrjSrb5WPwS+1VxDMaMH1q2BuOxYm34rpH1HkLRQ+v5uY0s1DNkKLu1vLDLJ9oOVTlxUzULNOY1YgVOjDJhPQqui1XZBFRCBwiiLzQvgZTDbYT3VzbXm30DIVVF+zDbNegj8gJrfMUcw2TRDw= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1738711258; c=relaxed/simple; bh=HMt93Qvd72e7Vz2VoEDR2dGKT3tO6E4ZXpdIP3TDNc8=; h=From:To:CC:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=fix1yEC+v+zjEJ+ybdesu8GWdRoJpC5pvFHYo6NR6osEg910ZOrlstxAUWTjpf0be6wqmJp7xeGunlXDweKYte92qqs1hNEcqLRBPVPV7robyTD44npTCljCWOhh2hFg03uyiS/+h0Z4SLwrzHhN6cd2shGwndMrVI6E5neJ20I= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=opensource.cirrus.com; spf=pass smtp.mailfrom=opensource.cirrus.com; dkim=pass (2048-bit key) header.d=cirrus.com header.i=@cirrus.com header.b=EEGmVRt5; arc=none smtp.client-ip=67.231.149.25 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=opensource.cirrus.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=opensource.cirrus.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=cirrus.com header.i=@cirrus.com header.b="EEGmVRt5" Received: from pps.filterd (m0077473.ppops.net [127.0.0.1]) by mx0a-001ae601.pphosted.com (8.18.1.2/8.18.1.2) with ESMTP id 514Fmw8M029853; Tue, 4 Feb 2025 17:20:17 -0600 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cirrus.com; h=cc :content-transfer-encoding:content-type:date:from:in-reply-to :message-id:mime-version:references:subject:to; s= PODMain02222019; bh=/f6FTRfiMITsj7S7AFHQkwLB5hayI/diyUQ2xzJa794=; b= EEGmVRt5X9MkMBwMkAPwI4wV06tGRRMZdhTGXWNBAKhVzDEdwR4paPcB41OvFzzw e5in5bHSEUElOYDQ5AjqZcpGG//NOlikMDVUrOJM9prHKuVHlQxdWstZ2hNpaWin fHc3MeBNG2e+yNi+Z7/VGfdZPGsIhnt/ncBFSbjJVN0FFwJaUQR6Oemn1Qe2NysU M+SUVco0ubUvl0uhNi2TuVd1uX+odjiSMZW8GOb/O2vCkE92hdWkzLjpEuTMAN/J TsRqGg+KsiGxyVPYWydmkE0VeZT1rNSiCHOcZRLn1RH2iBFc884OpafLax0zjSrn 9GzgyAQHobFn+S5jT5rr+w== Received: from ediex02.ad.cirrus.com ([84.19.233.68]) by mx0a-001ae601.pphosted.com (PPS) with ESMTPS id 44hhw53pkk-1 (version=TLSv1.2 cipher=ECDHE-RSA-AES256-GCM-SHA384 bits=256 verify=NOT); Tue, 04 Feb 2025 17:20:16 -0600 (CST) Received: from ediex02.ad.cirrus.com (198.61.84.81) by ediex02.ad.cirrus.com (198.61.84.81) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.2.1544.14; Tue, 4 Feb 2025 23:20:13 +0000 Received: from ediswmail9.ad.cirrus.com (198.61.86.93) by anon-ediex02.ad.cirrus.com (198.61.84.81) with Microsoft SMTP Server id 15.2.1544.14 via Frontend Transport; Tue, 4 Feb 2025 23:20:08 +0000 Received: from ftrev.crystal.cirrus.com (ftrev.ad.cirrus.com [141.131.145.81]) by ediswmail9.ad.cirrus.com (Postfix) with ESMTP id 5DB30820270; Tue, 4 Feb 2025 23:20:04 +0000 (UTC) From: Fred Treven To: Lee Jones , Rob Herring , "Krzysztof Kozlowski" , Conor Dooley , "Simon Trimmer" , Charles Keepax , Richard Fitzgerald , Dmitry Torokhov , James Ogletree , Ben Bright , Liam Girdwood , Mark Brown , Jaroslav Kysela , Takashi Iwai , David Rhodes , Jeff LaBundy , Heiko Stuebner , Karel Balej , Igor Prusov , Jack Yu , Weidong Wang , Binbin Zhou , Prasad Kumpatla , "Paul Handrigan" , Masahiro Yamada , Nuno Sa , Fred Treven CC: , , , , , Subject: [PATCH RESEND 5/7] mfd: cs40l26: Add support for CS40L26 core driver Date: Tue, 4 Feb 2025 17:18:34 -0600 Message-ID: <20250204231835.2000457-6-ftreven@opensource.cirrus.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20250204231835.2000457-1-ftreven@opensource.cirrus.com> References: <20250204231835.2000457-1-ftreven@opensource.cirrus.com> Precedence: bulk X-Mailing-List: linux-input@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Proofpoint-ORIG-GUID: SPOiO2LvLNFZ5ZQZ73Ku65ybGAjSEO5O X-Authority-Analysis: v=2.4 cv=W/3CVQWk c=1 sm=1 tr=0 ts=67a2a0b0 cx=c_pps a=uGhh+3tQvKmCLpEUO+DX4w==:117 a=uGhh+3tQvKmCLpEUO+DX4w==:17 a=T2h4t0Lz3GQA:10 a=w1d2syhTAAAA:8 a=kJREcIERN5GkY3D7nBAA:9 a=YXXWInSmI4Sqt1AkVdoW:22 X-Proofpoint-GUID: SPOiO2LvLNFZ5ZQZ73Ku65ybGAjSEO5O X-Proofpoint-Spam-Reason: safe Introduce support for Cirrus Logic Device CS40L26: A boosted haptic driver with integrated DSP and waveform memory with advanced closed loop algorithms and LRA protection. Signed-off-by: Fred Treven --- drivers/mfd/Kconfig | 29 + drivers/mfd/Makefile | 4 + drivers/mfd/cs40l26-core.c | 1412 +++++++++++++++++++++++++++++++++++ drivers/mfd/cs40l26-i2c.c | 63 ++ drivers/mfd/cs40l26-spi.c | 63 ++ include/linux/mfd/cs40l26.h | 341 +++++++++ 6 files changed, 1912 insertions(+) create mode 100644 drivers/mfd/cs40l26-core.c create mode 100644 drivers/mfd/cs40l26-i2c.c create mode 100644 drivers/mfd/cs40l26-spi.c create mode 100644 include/linux/mfd/cs40l26.h diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig index 6b0682af6e32..93a60fa9551a 100644 --- a/drivers/mfd/Kconfig +++ b/drivers/mfd/Kconfig @@ -2293,6 +2293,35 @@ config MCP_UCB1200_TS endmenu +config MFD_CS40L26_CORE + tristate + select MFD_CORE + select FW_CS_DSP + +config MFD_CS40L26_I2C + tristate "Cirrus Logic CS40L26 (I2C)" + select REGMAP_I2C + select MFD_CS40L26_CORE + depends on I2C + help + Select this to support the Cirrus Logic CS40L26 Haptic + Driver over I2C. + + This driver can be built as a module. If built as a module it will be + called "cs40l26-i2c". + +config MFD_CS40L26_SPI + tristate "Cirrus Logic CS40L26 (SPI)" + select REGMAP_SPI + select MFD_CS40L26_CORE + depends on SPI + help + Select this to support the Cirrus Logic CS40L26 Haptic + Driver over SPI. + + This driver can be built as a module. If built as a module it will be + called "cs40l26-spi". + config MFD_CS40L50_CORE tristate select MFD_CORE diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile index 9220eaf7cf12..8a245f36d73d 100644 --- a/drivers/mfd/Makefile +++ b/drivers/mfd/Makefile @@ -90,6 +90,10 @@ obj-$(CONFIG_MFD_MADERA) += madera.o obj-$(CONFIG_MFD_MADERA_I2C) += madera-i2c.o obj-$(CONFIG_MFD_MADERA_SPI) += madera-spi.o +obj-$(CONFIG_MFD_CS40L26_CORE) += cs40l26-core.o +obj-$(CONFIG_MFD_CS40L26_I2C) += cs40l26-i2c.o +obj-$(CONFIG_MFD_CS40L26_SPI) += cs40l26-spi.o + obj-$(CONFIG_MFD_CS40L50_CORE) += cs40l50-core.o obj-$(CONFIG_MFD_CS40L50_I2C) += cs40l50-i2c.o obj-$(CONFIG_MFD_CS40L50_SPI) += cs40l50-spi.o diff --git a/drivers/mfd/cs40l26-core.c b/drivers/mfd/cs40l26-core.c new file mode 100644 index 000000000000..b314f820de1e --- /dev/null +++ b/drivers/mfd/cs40l26-core.c @@ -0,0 +1,1412 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * CS40L26 Advanced Haptic Driver with waveform memory, + * integrated DSP, and closed-loop algorithms + * + * Copyright 2025 Cirrus Logic, Inc. + * + * Author: Fred Treven + */ + +#include +#include +#include +#include +#include + +static const struct mfd_cell cs40l26_devs[] = { + { .name = "cs40l26-codec", }, + { .name = "cs40l26-vibra", }, +}; + +const struct regmap_config cs40l26_regmap = { + .reg_bits = 32, + .val_bits = 32, + .reg_stride = 4, + .reg_format_endian = REGMAP_ENDIAN_BIG, + .val_format_endian = REGMAP_ENDIAN_BIG, + .max_register = CS40L26_LASTREG, + .cache_type = REGCACHE_NONE, +}; +EXPORT_SYMBOL_GPL(cs40l26_regmap); + +static const char *const cs40l26_supplies[] = { + "va", "vp", +}; + +inline void cs40l26_pm_exit(struct device *dev) +{ + pm_runtime_mark_last_busy(dev); + pm_runtime_put_autosuspend(dev); +} +EXPORT_SYMBOL_GPL(cs40l26_pm_exit); + +static int cs40l26_fw_write_raw(struct cs_dsp *dsp, const char *const name, + const unsigned int algo_id, const u32 offset_words, + const size_t len_words, u32 *buf) +{ + struct cs_dsp_coeff_ctl *ctl; + __be32 *val; + int i, ret; + + ctl = cs_dsp_get_ctl(dsp, name, WMFW_ADSP2_XM, algo_id); + if (!ctl) { + dev_err(dsp->dev, "Failed to find FW control %s\n", name); + return -EINVAL; + } + + val = kzalloc(len_words * sizeof(u32), GFP_KERNEL); + if (!val) + return -ENOMEM; + + for (i = 0; i < len_words; i++) + val[i] = cpu_to_be32(buf[i]); + + ret = cs_dsp_coeff_write_ctrl(ctl, offset_words, val, len_words * sizeof(u32)); + if (ret < 0) + dev_err(dsp->dev, "Failed to write FW control %s\n", name); + + kfree(val); + + return (ret < 0) ? ret : 0; +} + +inline int cs40l26_fw_write(struct cs_dsp *dsp, const char *const name, const unsigned int algo_id, + u32 val) +{ + return cs40l26_fw_write_raw(dsp, name, algo_id, 0, 1, &val); +} +EXPORT_SYMBOL_GPL(cs40l26_fw_write); + +static int cs40l26_fw_read_raw(struct cs_dsp *dsp, const char *const name, + const unsigned int algo_id, const unsigned int offset_words, + const size_t len_words, u32 *buf) +{ + struct cs_dsp_coeff_ctl *ctl; + int i, ret; + + ctl = cs_dsp_get_ctl(dsp, name, WMFW_ADSP2_XM, algo_id); + if (!ctl) { + dev_err(dsp->dev, "Failed to find FW control %s\n", name); + return -EINVAL; + } + + ret = cs_dsp_coeff_read_ctrl(ctl, offset_words, buf, len_words * sizeof(u32)); + if (ret) { + dev_err(dsp->dev, "Failed to read FW control %s\n", name); + return ret; + } + + for (i = 0; i < len_words; i++) + buf[i] = be32_to_cpu(buf[i]); + + return 0; +} + +inline int cs40l26_fw_read(struct cs_dsp *dsp, const char *const name, const unsigned int algo_id, + u32 *buf) +{ + return cs40l26_fw_read_raw(dsp, name, algo_id, 0, 1, buf); +} +EXPORT_SYMBOL_GPL(cs40l26_fw_read); + +static struct cs40l26_irq *cs40l26_get_irq(struct cs40l26 *cs40l26, const int num, const int bit); + +static int cs40l26_gpio1_rise_irq(void *data) +{ + struct cs40l26 *cs40l26 = data; + + if (cs40l26->wksrc_sts & CS40L26_WKSRC_STS_EN) + dev_dbg(cs40l26->dev, "GPIO1 Rising Edge Detected\n"); + + cs40l26->wksrc_sts |= CS40L26_WKSRC_STS_EN; + + return 0; +} + +static int cs40l26_gpio1_fall_irq(void *data) +{ + struct cs40l26 *cs40l26 = data; + + if (cs40l26->wksrc_sts & CS40L26_WKSRC_STS_EN) + dev_dbg(cs40l26->dev, "GPIO1 Falling Edge Detected\n"); + + cs40l26->wksrc_sts |= CS40L26_WKSRC_STS_EN; + + return 0; +} + +static int cs40l26_wksrc_any_irq(void *data) +{ + struct cs40l26 *cs40l26 = data; + u32 last_wksrc, pwrmgt_sts; + int ret; + + guard(mutex)(&cs40l26->dsp.pwr_lock); + + ret = regmap_read(cs40l26->regmap, CS40L26_PWRMGT_STS, &pwrmgt_sts); + if (ret) + return ret; + + cs40l26->wksrc_sts = (u8)FIELD_GET(CS40L26_WKSRC_STS_MASK, pwrmgt_sts); + + ret = cs40l26_fw_read(&cs40l26->dsp, "LAST_WAKESRC_CTL", cs40l26->dsp.fw_id, &last_wksrc); + if (ret) + return ret; + + cs40l26->last_wksrc_pol = (u8)(last_wksrc & CS40L26_WKSRC_GPIO_POL_MASK); + + return 0; +} + +static int cs40l26_wksrc_gpio1_irq(void *data) +{ + struct cs40l26 *cs40l26 = data; + + /* + * The GPIO wakesource and event interrupts are not able to reliably determine + * the GPIO edge that triggered the interrupt (rising/falling). + * + * The driver must therefore perform this logic in order to determine the edge + * of the GPIO event for two cases: + * 1. The GPIO event is waking the device from hibernation. + * 2. The GPIO event occurs when the device is already awake. + */ + + if (cs40l26->wksrc_sts & cs40l26->last_wksrc_pol) { + dev_dbg(cs40l26->dev, "GPIO1 Falling Edge Detected\n"); + cs40l26->wksrc_sts |= CS40L26_WKSRC_STS_EN; + } else { + dev_dbg(cs40l26->dev, "GPIO1 rising edge detected\n"); + } + + return 0; +} + +static int cs40l26_error_release(struct cs40l26 *cs40l26, const enum cs40l26_error err) +{ + int ret; + + dev_err(cs40l26->dev, "Device Reported Error: %u\n", (unsigned int)BIT(err)); + + ret = regmap_clear_bits(cs40l26->regmap, CS40L26_ERROR_RELEASE, BIT(err)); + if (ret) + return ret; + + ret = regmap_set_bits(cs40l26->regmap, CS40L26_ERROR_RELEASE, BIT(err)); + if (ret) + return ret; + + return regmap_clear_bits(cs40l26->regmap, CS40L26_ERROR_RELEASE, BIT(err)); +} + +static int cs40l26_bst_ovp_err_irq(void *data) +{ + struct cs40l26 *cs40l26 = data; + + return cs40l26_error_release(cs40l26, CS40L26_ERROR_BST_OVP); +} + +static int cs40l26_bst_dcm_uvp_err_irq(void *data) +{ + struct cs40l26 *cs40l26 = data; + + return cs40l26_error_release(cs40l26, CS40L26_ERROR_BST_DCM_UVP); +} + +static int cs40l26_bst_short_err_irq(void *data) +{ + struct cs40l26 *cs40l26 = data; + + return cs40l26_error_release(cs40l26, CS40L26_ERROR_BST_SHORT); +} + +static int cs40l26_temp_warn_rise_irq(void *data) +{ + struct cs40l26 *cs40l26 = data; + + return cs40l26_error_release(cs40l26, CS40L26_ERROR_TEMP_WARN); +} + +static int cs40l26_temp_err_irq(void *data) +{ + struct cs40l26 *cs40l26 = data; + + return cs40l26_error_release(cs40l26, CS40L26_ERROR_TEMP_ERR); +} + +static int cs40l26_amp_short_err_irq(void *data) +{ + struct cs40l26 *cs40l26 = data; + + return cs40l26_error_release(cs40l26, CS40L26_ERROR_AMP_SHORT); +} + +static int cs40l26_dsp_queue_buffer_read(struct cs40l26 *cs40l26, u32 *val) +{ + u32 queue_rd, queue_wt, sts; + int ret; + + guard(mutex)(&cs40l26->dsp.pwr_lock); + + ret = cs40l26_fw_read(&cs40l26->dsp, "QUEUE_WT", CS40L26_DSP_ALGO_ID, &queue_wt); + if (ret) + return ret; + + ret = cs40l26_fw_read(&cs40l26->dsp, "QUEUE_RD", CS40L26_DSP_ALGO_ID, &queue_rd); + if (ret) + return ret; + + if (queue_rd - sizeof(u32) == queue_wt) { + ret = cs40l26_fw_read(&cs40l26->dsp, "STATUS", CS40L26_DSP_ALGO_ID, &sts); + if (ret) + return ret; + + if (sts) { + dev_err(cs40l26->dev, "DSP Queue Buffer is full, message(s) missed\n"); + return -ENOSPC; + } + } + + if (queue_rd == queue_wt) /* DSP Queue is Empty */ + return 1; + + ret = regmap_read(cs40l26->regmap, queue_rd, val); + if (ret) + return ret; + + if (queue_rd == cs40l26->queue_last) + queue_rd = cs40l26->queue_base; + else + queue_rd += sizeof(u32); + + return cs40l26_fw_write(&cs40l26->dsp, "QUEUE_RD", CS40L26_DSP_ALGO_ID, queue_rd); +} + +static int cs40l26_dsp_queue_irq(void *data) +{ + struct cs40l26 *cs40l26 = data; + bool end = false; + int ret; + u32 val; + + ret = cs40l26_dsp_queue_buffer_read(cs40l26, &val); + if (ret == 1) + return 0; + else if (ret) + return ret; + + while (!end) { + if ((val & CS40L26_DSP_CMD_INDEX_MASK) == CS40L26_DSP_PANIC) { + dev_err(cs40l26->dev, "DSP Panic! Error: 0x%06X\n", + (u32)(val & CS40L26_DSP_CMD_PAYLOAD_MASK)); + return -ENOTRECOVERABLE; + } + + switch (val) { + case CS40L26_DSP_COMPLETE_CP: + dev_dbg(cs40l26->dev, "DSP Queue: Control Port Haptics Completed\n"); + break; + case CS40L26_DSP_COMPLETE_I2S: + dev_dbg(cs40l26->dev, "DSP Queue: I2S Stream Completed\n"); + break; + case CS40L26_DSP_TRIGGER_CP: + dev_dbg(cs40l26->dev, "DSP Queue: Control Port Haptics Triggered\n"); + break; + case CS40L26_DSP_TRIGGER_I2S: + dev_dbg(cs40l26->dev, "DSP Queue: I2S Stream Triggered\n"); + break; + case CS40L26_DSP_PM_AWAKE: + cs40l26->wksrc_sts |= CS40L26_WKSRC_STS_EN; + dev_dbg(cs40l26->dev, "DSP Queue: AWAKE\n"); + break; + case CS40L26_DSP_SYS_ACK: + dev_dbg(cs40l26->dev, "DSP Queue: Inbound PING received\n"); + break; + default: + dev_err(cs40l26->dev, "DSP Queue value (0x%X) unrecognized\n", val); + return -EINVAL; + } + + ret = cs40l26_dsp_queue_buffer_read(cs40l26, &val); + if (ret == 1) + end = true; + else if (ret) + return ret; + } + + return 0; +} + +static struct reg_sequence cs40l26_irq_masks[] = { + REG_SEQ0(CS40L26_IRQ1_MASK_1, CS40L26_IRQ_1_ALL_MASKED), + REG_SEQ0(CS40L26_IRQ1_MASK_2, CS40L26_IRQ_2_ALL_MASKED), +}; + +static void cs40l26_irq_unmask(struct cs40l26 *cs40l26, const int num, const int virq) +{ + struct cs40l26_irq *irq; + + if (num != 1 && num != 2) { + dev_err(cs40l26->dev, "Invalid IRQ number %d\n", num); + return; + } + + irq = cs40l26_get_irq(cs40l26, num, virq); + if (!irq) + return; + + cs40l26->irq_masks[num - 1].def &= ~irq->mask; +} + +static struct cs40l26_irq cs40l26_irqs_1[] = { + CS40L26_IRQ(GPIO1_RISE, "GPIO1 Rise", cs40l26_gpio1_rise_irq), + CS40L26_IRQ(GPIO1_FALL, "GPIO1 Fall", cs40l26_gpio1_fall_irq), + CS40L26_IRQ(WKSRC_STS_ANY, "ANY Wake", cs40l26_wksrc_any_irq), + CS40L26_IRQ(WKSRC_STS_GPIO1, "GPIO1 Wake", cs40l26_wksrc_gpio1_irq), + CS40L26_IRQ(WKSRC_STS_SPI, "SPI Wake", NULL), + CS40L26_IRQ(WKSRC_STS_I2C, "I2C Wake", NULL), + CS40L26_IRQ(BST_OVP_FLAG_RISE, "BST OVP Rise", NULL), + CS40L26_IRQ(BST_OVP_FLAG_FALL, "BST OVP Fall", NULL), + CS40L26_IRQ(BST_OVP_ERR, "BST OVP Error", cs40l26_bst_ovp_err_irq), + CS40L26_IRQ(BST_DCM_UVP_ERR, "BST UVP Error", cs40l26_bst_dcm_uvp_err_irq), + CS40L26_IRQ(BST_SHORT_ERR, "BST Short Error", cs40l26_bst_short_err_irq), + CS40L26_IRQ(BST_IPK_FLAG, "BST IPK Flag", NULL), + CS40L26_IRQ(TEMP_WARN_RISE, "TEMP Warn Rise", cs40l26_temp_warn_rise_irq), + CS40L26_IRQ(TEMP_WARN_FALL, "TEMP Warn Fall", NULL), + CS40L26_IRQ(TEMP_ERR, "TEMP Error", cs40l26_temp_err_irq), + CS40L26_IRQ(AMP_ERR, "AMP Error", cs40l26_amp_short_err_irq), + CS40L26_IRQ(DSP_RX_QUEUE, "DSP Rx", cs40l26_dsp_queue_irq), +}; + +static struct cs40l26_irq cs40l26_irqs_2[] = { + CS40L26_IRQ(REFCLK_PRESENT, "REFCLK Present", NULL), + CS40L26_IRQ(REFCLK_MISSING_FALL, "REFCLK Missing Fall", NULL), + CS40L26_IRQ(REFCLK_MISSING_RISE, "REFCLK Missing Rise", NULL), + CS40L26_IRQ(VPMON_CLIPPED, "VPMON Clipped", NULL), + CS40L26_IRQ(VBSTMON_CLIPPED, "VBSTMON Clipped", NULL), + CS40L26_IRQ(VMON_CLIPPED, "VMON Clipped", NULL), + CS40L26_IRQ(IMON_CLIPPED, "IMON Clipped", NULL), +}; + +static struct cs40l26_irq *cs40l26_get_irq(struct cs40l26 *cs40l26, const int num, const int bit) +{ + int i; + + if (num == 1) { + for (i = 0; i < ARRAY_SIZE(cs40l26_irqs_1); i++) { + if (cs40l26_irqs_1[i].virq == bit) + return &cs40l26_irqs_1[i]; + } + } else if (num == 2) { + for (i = 0; i < ARRAY_SIZE(cs40l26_irqs_2); i++) { + if (cs40l26_irqs_2[i].virq == bit) + return &cs40l26_irqs_2[i]; + } + } else { + dev_err(cs40l26->dev, "Invalid IRQ number %d\n", num); + return NULL; + } + + dev_err(cs40l26->dev, "Failed to find IRQ corresponding to bit in IRQ%d %d\n", bit, num); + + return NULL; +} + +static irqreturn_t cs40l26_irq_handler(int irq, void *data) +{ + struct cs40l26 *cs40l26 = data; + struct cs40l26_irq *irq_s; + unsigned long handle_bits; + u32 eint, mask, sts; + int i, j, ret; + + if (pm_runtime_resume_and_get(cs40l26->dev)) { + dev_err(cs40l26->dev, "Failed to exit hibernate to service interrupt\n"); + return IRQ_NONE; + } + + guard(mutex)(&cs40l26->lock); + + ret = regmap_read(cs40l26->regmap, CS40L26_IRQ1_STATUS, &sts); + if (ret) + goto err_pm; + + if (!(sts & CS40L26_IRQ_STATUS_ASSERT)) { + dev_err(cs40l26->dev, "IRQ1 asserted with no pending interrupts\n"); + ret = -EIO; + goto err_pm; + } + + for (j = 0; j < 2; j++) { + ret = regmap_read(cs40l26->regmap, CS40L26_IRQ1_MASK_1 + j * 4, &mask); + if (ret) + goto err_pm; + + ret = regmap_read(cs40l26->regmap, CS40L26_IRQ1_EINT_1 + j * 4, &eint); + if (ret) + goto err_pm; + + handle_bits = eint & ~mask; + + for_each_set_bit(i, &handle_bits, j ? CS40L26_IRQ_2_NBITS : CS40L26_IRQ_1_NBITS) { + irq_s = cs40l26_get_irq(cs40l26, j + 1, i); + if (!irq_s) + continue; + + dev_dbg(cs40l26->dev, "%s", irq_s->name); + + if (irq_s->handler) { + ret = irq_s->handler(cs40l26); + if (ret) + goto err_pm; + } + + ret = regmap_write(cs40l26->regmap, CS40L26_IRQ1_EINT_1 + j * 4, BIT(i)); + if (ret) + goto err_pm; + } + } + +err_pm: + cs40l26_pm_exit(cs40l26->dev); + + return IRQ_RETVAL(ret); +} + +int cs40l26_dsp_write(struct cs40l26 *cs40l26, const u32 val) +{ + int i, ret; + u32 ack; + + /* Device NAKs if hibernating, so retry if this is the case */ + for (i = 0; i < CS40L26_DSP_TIMEOUT_COUNT; i++) { + ret = regmap_write(cs40l26->regmap, CS40L26_DSP_QUEUE, val); + if (!ret) + break; + + usleep_range(CS40L26_DSP_POLL_US, CS40L26_DSP_POLL_US + 100); + } + + if (i == CS40L26_DSP_TIMEOUT_COUNT) { + dev_err(cs40l26->dev, "Timed out writing %#X to DSP\n", val); + return -ETIMEDOUT; + } + + ret = regmap_read_poll_timeout(cs40l26->regmap, CS40L26_DSP_QUEUE, ack, !ack, + CS40L26_DSP_POLL_US, + CS40L26_DSP_POLL_US * CS40L26_DSP_TIMEOUT_COUNT); + if (ret) + dev_err(cs40l26->dev, "DSP failed to ACK %#X: %d\n", val, ret); + + return ret; +} +EXPORT_SYMBOL_GPL(cs40l26_dsp_write); + +int cs40l26_dsp_state_get(struct cs40l26 *cs40l26, u32 *state) +{ + u32 dsp_state = CS40L26_DSP_STATE_NONE; + int i, ret; + + if (cs40l26->dsp.running) { + for (i = 0; i < CS40L26_DSP_TIMEOUT_COUNT; i++) { + ret = cs40l26_fw_read(&cs40l26->dsp, "PM_CUR_STATE", CS40L26_PM_ALGO_ID, + &dsp_state); + if (ret) + return ret; + + if (dsp_state != CS40L26_DSP_STATE_NONE) + break; + + usleep_range(CS40L26_DSP_POLL_US, CS40L26_DSP_POLL_US + 100); + } + + if (i == CS40L26_DSP_TIMEOUT_COUNT) { + dev_err(cs40l26->dev, "Timed out reading PM_CUR_STATE\n"); + return -ETIMEDOUT; + } + } else { + ret = regmap_read_poll_timeout(cs40l26->regmap, + cs40l26->variant->info->pm_cur_state, dsp_state, + dsp_state != CS40L26_DSP_STATE_NONE, + CS40L26_DSP_POLL_US, + CS40L26_DSP_POLL_US * CS40L26_DSP_TIMEOUT_COUNT); + if (ret) { + dev_err(cs40l26->dev, "Failed to read poll for static PM_CUR_STATE\n"); + return ret; + } + } + + *state = dsp_state; + + return 0; +} +EXPORT_SYMBOL_GPL(cs40l26_dsp_state_get); + +static bool cs40l26_dsp_can_run(struct cs40l26 *cs40l26) +{ + struct regmap *regmap = cs40l26->regmap; + u32 dsp_state, pm_state_locks; + int ret; + + ret = cs40l26_dsp_state_get(cs40l26, &dsp_state); + if (ret) + return false; + + if (dsp_state == CS40L26_DSP_STATE_ACTIVE) + return true; + + if (dsp_state != CS40L26_DSP_STATE_STANDBY) { + dev_err(cs40l26->dev, "DSP in bad state: %u\n", dsp_state); + return false; + } + + if (cs40l26->dsp.running) + ret = cs40l26_fw_read_raw(&cs40l26->dsp, "PM_STATE_LOCKS", CS40L26_PM_ALGO_ID, + CS40L26_DSP_LOCK3_OFFSET_WORDS, 1, &pm_state_locks); + else + ret = regmap_read(regmap, cs40l26->variant->info->pm_state_locks3, &pm_state_locks); + + if (ret) { + dev_err(cs40l26->dev, "Failed to read PM_STATE_LOCKS\n"); + return false; + } + + return pm_state_locks & CS40L26_DSP_LOCK3_MASK; +} + +static int cs40l26_prevent_hiber(struct cs40l26 *cs40l26) +{ + int i, ret; + + for (i = 0; i < CS40L26_DSP_TIMEOUT_COUNT; i++) { + ret = cs40l26_dsp_write(cs40l26, CS40L26_DSP_CMD_PREVENT_HIBER); + if (ret) + return ret; + + usleep_range(CS40L26_DSP_POLL_US, CS40L26_DSP_POLL_US + 100); + + if (cs40l26_dsp_can_run(cs40l26)) + break; + } + + if (i == CS40L26_DSP_TIMEOUT_COUNT) { + dev_err(cs40l26->dev, "Failed to prevent hibernation\n"); + return -ETIMEDOUT; + } + + return 0; +} + +static int cs40l26_lbst_short_test(struct cs40l26 *cs40l26) +{ + u32 err, vbst_ctl_1, vbst_ctl_2; + int ret; + + /* Read initial values to restore after test is complete */ + ret = regmap_read(cs40l26->regmap, CS40L26_VBST_CTL_2, &vbst_ctl_2); + if (ret) + return ret; + + ret = regmap_read(cs40l26->regmap, CS40L26_VBST_CTL_1, &vbst_ctl_1); + if (ret) + return ret; + + ret = regmap_update_bits(cs40l26->regmap, CS40L26_VBST_CTL_2, CS40L26_BST_CTL_SEL_MASK, + CS40L26_BST_CTL_SEL_FIXED); + if (ret) + return ret; + + ret = regmap_update_bits(cs40l26->regmap, CS40L26_VBST_CTL_1, CS40L26_BST_CTL_MASK, + CS40L26_BST_CTL_VP); + if (ret) + return ret; + + ret = regmap_set_bits(cs40l26->regmap, CS40L26_GLOBAL_ENABLES, CS40L26_GLOBAL_EN); + if (ret) + return ret; + + /* Wait for boost converter to power up */ + usleep_range(CS40L26_BST_TIME_US, CS40L26_BST_TIME_US + 100); + + ret = regmap_read(cs40l26->regmap, CS40L26_ERROR_RELEASE, &err); + if (ret) + return ret; + + if (err & BIT(CS40L26_ERROR_BST_SHORT)) { + dev_err(cs40l26->dev, "Boost shorted at startup\n"); + return -ENOTRECOVERABLE; + } + + /* Return to previous state before test */ + ret = regmap_clear_bits(cs40l26->regmap, CS40L26_GLOBAL_ENABLES, CS40L26_GLOBAL_EN); + if (ret) + return ret; + + ret = regmap_write(cs40l26->regmap, CS40L26_VBST_CTL_1, vbst_ctl_1); + if (ret) + return ret; + + return regmap_write(cs40l26->regmap, CS40L26_VBST_CTL_2, vbst_ctl_2); +} + +static const struct reg_sequence cs40l26_a1_b1_errata[] = { + { CS40L26_PLL_REFCLK_DETECT_0, CS40L26_PLL_REFCLK_DET_DISABLE }, + { 0x00000040, 0x00000055 }, + { 0x00000040, 0x000000AA }, + { CS40L26_TEST_LBST, CS40L26_DISABLE_EXPL_MODE }, +}; + +static int cs40l26_a1_b1_handle_errata(struct cs40l26 *cs40l26) +{ + int ret; + + /* + * Boost Exploratory Mode must be disabled on 0xA1/0xB1 devices in order to ensure there is + * no unintentional damage to the boost inductor. Any boost short that occurs after the + * LBST short test at probe will not be detected. + */ + ret = cs40l26_lbst_short_test(cs40l26); + if (ret) + return ret; + + ret = regmap_multi_reg_write(cs40l26->regmap, cs40l26_a1_b1_errata, + ARRAY_SIZE(cs40l26_a1_b1_errata)); + if (ret) + return ret; + + return cs_dsp_wseq_multi_write(&cs40l26->dsp, &cs40l26->wseqs[CS40L26_WSEQ_POWER_ON], + cs40l26_a1_b1_errata, ARRAY_SIZE(cs40l26_a1_b1_errata), + CS_DSP_WSEQ_FULL, false); +} + +static const struct cs40l26_variant_info cs40l26_a1_b1_info = { + .pm_cur_state = CS40L26_A1_B1_PM_CUR_STATE, + .pm_state_locks = CS40L26_A1_B1_PM_STATE_LOCKS, + .pm_state_locks3 = CS40L26_A1_B1_PM_STATE_LOCKS3, + .pm_stdby_ticks = CS40L26_A1_B1_PM_STDBY_TICKS, + .pm_active_ticks = CS40L26_A1_B1_PM_ACTIVE_TICKS, + .halo_state = CS40L26_A1_B1_HALO_STATE, + .event_map_1 = CS40L26_A1_B1_EVENT_MAP_1, + .event_map_2 = CS40L26_A1_B1_EVENT_MAP_2, + .fw_min_rev = CS40L26_FW_A1_B1_MIN_REV, + .ram_ext_algo_id = CS40L26_EXT_ALGO_ID, + .vibegen_algo_id = CS40L26_VIBEGEN_ALGO_ID_A1, +}; + +static const struct cs40l26_variant_info cs40l26_b2_info = { + .pm_cur_state = CS40L26_B2_PM_CUR_STATE, + .pm_state_locks = CS40L26_B2_PM_STATE_LOCKS, + .pm_state_locks3 = CS40L26_B2_PM_STATE_LOCKS3, + .pm_stdby_ticks = CS40L26_B2_PM_STDBY_TICKS, + .pm_active_ticks = CS40L26_B2_PM_ACTIVE_TICKS, + .halo_state = CS40L26_B2_HALO_STATE, + .event_map_1 = CS40L26_B2_EVENT_MAP_1, + .event_map_2 = CS40L26_B2_EVENT_MAP_2, + .fw_min_rev = CS40L26_FW_B2_MIN_REV, + .ram_ext_algo_id = CS40L26_FW_ID, + .vibegen_algo_id = CS40L26_VIBEGEN_ALGO_ID_B2, +}; + +static const struct cs40l26_variant cs40l26_a1_b1_variant = { + .info = &cs40l26_a1_b1_info, + .handle_errata = &cs40l26_a1_b1_handle_errata, +}; + +static const struct cs40l26_variant cs40l26_b2_variant = { + .info = &cs40l26_b2_info, + .handle_errata = NULL, +}; + +static inline int cs40l26_pm_timeout_ms_set(struct cs40l26 *cs40l26, const u32 dsp_state, + const u32 timeout_ms) +{ + return regmap_write(cs40l26->regmap, + dsp_state == CS40L26_DSP_STATE_STANDBY ? + cs40l26->variant->info->pm_stdby_ticks : + cs40l26->variant->info->pm_active_ticks, + (timeout_ms * CS40L26_PM_TICKS_PER_SEC) / 1000); +} + +static int cs40l26_pm_timeout_ms_get(struct cs40l26 *cs40l26, const u32 dsp_state, u32 *timeout_ms) +{ + u32 timeout_ticks; + int ret; + + ret = regmap_read(cs40l26->regmap, + dsp_state == CS40L26_DSP_STATE_STANDBY ? + cs40l26->variant->info->pm_stdby_ticks : + cs40l26->variant->info->pm_active_ticks, + &timeout_ticks); + if (ret) + return ret; + + *timeout_ms = DIV_ROUND_UP(timeout_ticks * 1000, CS40L26_PM_TICKS_PER_SEC); + + return 0; +} + +static int cs40l26_pm_runtime_setup(struct device *dev) +{ + int ret; + + pm_runtime_set_autosuspend_delay(dev, CS40L26_AUTOSUSPEND_DELAY_MS); + pm_runtime_use_autosuspend(dev); + pm_runtime_get_noresume(dev); + ret = pm_runtime_set_active(dev); + if (ret) + return ret; + + return devm_pm_runtime_enable(dev); +} + +static int cs40l26_dsp_pre_config(struct cs40l26 *cs40l26) +{ + u32 dsp_state, halo_state, timeout_ms; + int i, ret; + + ret = regmap_read(cs40l26->regmap, cs40l26->variant->info->halo_state, &halo_state); + if (ret) + return ret; + + if (halo_state != CS40L26_DSP_HALO_STATE_RUN) { + dev_err(cs40l26->dev, "Invalid DSP state: %u\n", halo_state); + return -EINVAL; + } + + ret = cs40l26_pm_timeout_ms_get(cs40l26, CS40L26_DSP_STATE_ACTIVE, &timeout_ms); + if (ret) { + dev_err(cs40l26->dev, "Failed to get ACTIVE timeout\n"); + return ret; + } + + for (i = 0; i < CS40L26_DSP_STATE_TIMEOUT_COUNT; i++) { + ret = cs40l26_dsp_state_get(cs40l26, &dsp_state); + if (ret) + return ret; + + if (dsp_state != CS40L26_DSP_STATE_SHUTDOWN && + dsp_state != CS40L26_DSP_STATE_STANDBY) + dev_warn(cs40l26->dev, "DSP core not safe to kill\n"); + else + break; + + usleep_range(timeout_ms * 1000, (timeout_ms * 1000) + 100); + } + + if (i == CS40L26_DSP_STATE_TIMEOUT_COUNT) { + dev_err(cs40l26->dev, "DSP Core could not be shut down\n"); + return -ETIMEDOUT; + } + + return regmap_write(cs40l26->regmap, CS40L26_DSP1_CCM_CORE_CONTROL, + CS40L26_DSP_CCM_CORE_KILL); +} + +static const struct cs_dsp_region cs40l26_dsp_regions[] = { + { .type = WMFW_HALO_PM_PACKED, .base = CS40L26_DSP1_PMEM_0 }, + { .type = WMFW_HALO_XM_PACKED, .base = CS40L26_DSP1_XMEM_PACKED_0 }, + { .type = WMFW_HALO_YM_PACKED, .base = CS40L26_DSP1_YMEM_PACKED_0 }, + { .type = WMFW_ADSP2_XM, .base = CS40L26_DSP1_XMEM_UNPACKED24_0 }, + { .type = WMFW_ADSP2_YM, .base = CS40L26_DSP1_YMEM_UNPACKED24_0 }, +}; + +static int cs40l26_get_model(struct cs40l26 *cs40l26) +{ + int ret; + + ret = regmap_read(cs40l26->regmap, CS40L26_DEVID, &cs40l26->devid); + if (ret) + return ret; + + ret = regmap_read(cs40l26->regmap, CS40L26_REVID, &cs40l26->revid); + if (ret) + return ret; + + switch (cs40l26->devid) { + case CS40L26_DEVID_L26: + if (cs40l26->revid != CS40L26_REVID_A1 && cs40l26->revid != CS40L26_REVID_B1) + goto err; + + cs40l26->variant = &cs40l26_a1_b1_variant; + break; + case CS40L26_DEVID_L27: + if (cs40l26->revid != CS40L26_REVID_B2) + goto err; + + cs40l26->variant = &cs40l26_b2_variant; + break; + default: + dev_err(cs40l26->dev, "Invalid device ID 0x%06X\n", cs40l26->devid); + return -EINVAL; + } + + dev_info(cs40l26->dev, "Cirrus Logic CS40L26 ID: 0x%06X, Revision: 0x%02X\n", + cs40l26->devid, cs40l26->revid); + + return 0; + +err: + dev_err(cs40l26->dev, "Invalid revision 0x%02X for device 0x%06X\n", cs40l26->revid, + cs40l26->devid); + return -EINVAL; +} + +int cs40l26_set_pll_loop(struct cs40l26 *cs40l26, const u32 pll_loop) +{ + int i; + + /* Retry in case DSP is hibernating */ + for (i = 0; i < CS40L26_PLL_NUM_SET_ATTEMPTS; i++) { + if (!regmap_update_bits(cs40l26->regmap, CS40L26_REFCLK_INPUT, + CS40L26_PLL_REFCLK_LOOP_MASK, + pll_loop << CS40L26_PLL_REFCLK_LOOP_SHIFT)) + break; + } + + if (i == CS40L26_PLL_NUM_SET_ATTEMPTS) { + dev_err(cs40l26->dev, "Failed to configure PLL\n"); + return -ETIMEDOUT; + } + + return 0; +} +EXPORT_SYMBOL_GPL(cs40l26_set_pll_loop); + +static int cs40l26_wseq_init(struct cs40l26 *cs40l26) +{ + struct cs_dsp *dsp = &cs40l26->dsp; + + cs40l26->wseqs[CS40L26_WSEQ_POWER_ON].ctl = + cs_dsp_get_ctl(dsp, "POWER_ON_SEQUENCE", WMFW_ADSP2_XM, CS40L26_PM_ALGO_ID); + if (!cs40l26->wseqs[CS40L26_WSEQ_POWER_ON].ctl) { + dev_err(cs40l26->dev, "POWER_ON write sequence not found\n"); + return -EINVAL; + } + + cs40l26->wseqs[CS40L26_WSEQ_ACTIVE].ctl = + cs_dsp_get_ctl(dsp, "ACTIVE_SEQUENCE", WMFW_ADSP2_XM, CS40L26_PM_ALGO_ID); + if (!cs40l26->wseqs[CS40L26_WSEQ_ACTIVE].ctl) { + dev_err(cs40l26->dev, "ACTIVE write sequence not found\n"); + return -EINVAL; + } + + cs40l26->wseqs[CS40L26_WSEQ_STANDBY].ctl = + cs_dsp_get_ctl(dsp, "STANDBY_SEQUENCE", WMFW_ADSP2_XM, CS40L26_PM_ALGO_ID); + if (!cs40l26->wseqs[CS40L26_WSEQ_STANDBY].ctl) { + dev_err(cs40l26->dev, "STANDBY write sequence not found\n"); + return -EINVAL; + } + + return cs_dsp_wseq_init(dsp, cs40l26->wseqs, CS40L26_NUM_WSEQS); +} + +static int cs40l26_wksrc_config(struct cs40l26 *cs40l26) +{ + u32 wksrc; + int ret; + + if (!strncmp(cs40l26->bus->name, "spi", strlen(cs40l26->bus->name))) { + cs40l26_irq_unmask(cs40l26, 1, CS40L26_IRQ_WKSRC_STS_SPI); + wksrc = CS40L26_WKSRC_POL_SPI | CS40L26_WKSRC_EN_SPI; + } else if (!strncmp(cs40l26->bus->name, "i2c", strlen(cs40l26->bus->name))) { + cs40l26_irq_unmask(cs40l26, 1, CS40L26_IRQ_WKSRC_STS_I2C); + wksrc = CS40L26_WKSRC_EN_I2C; + } else { + dev_err(cs40l26->dev, "Invalid bus type\n"); + return -EINVAL; + } + + cs40l26_irq_unmask(cs40l26, 1, CS40L26_IRQ_WKSRC_STS_GPIO1); + cs40l26_irq_unmask(cs40l26, 1, CS40L26_IRQ_WKSRC_STS_ANY); + + ret = regmap_write(cs40l26->regmap, CS40L26_WAKESRC_CTL, wksrc); + if (ret) + return ret; + + return cs_dsp_wseq_write(&cs40l26->dsp, &cs40l26->wseqs[CS40L26_WSEQ_POWER_ON], + CS40L26_WAKESRC_CTL, wksrc, CS_DSP_WSEQ_L16, true); +} + +static inline void cs40l26_gpio_config(struct cs40l26 *cs40l26) +{ + cs40l26_irq_unmask(cs40l26, 1, CS40L26_IRQ_GPIO1_RISE); + cs40l26_irq_unmask(cs40l26, 1, CS40L26_IRQ_GPIO1_FALL); +} + +static int cs40l26_bst_ipk_config(struct cs40l26 *cs40l26) +{ + u32 bst_ipk; + int ret; + + bst_ipk = (clamp_val(cs40l26->bst_ipk_ua, CS40L26_BST_IPK_UA_MIN, CS40L26_BST_IPK_UA_MAX) - + CS40L26_BST_IPK_UA_OFFSET) / CS40L26_BST_IPK_UA_STEP; + + cs40l26_irq_unmask(cs40l26, 1, CS40L26_IRQ_BST_IPK_FLAG); + + ret = regmap_write(cs40l26->regmap, CS40L26_BST_IPK_CTL, bst_ipk); + if (ret) + return ret; + + return cs_dsp_wseq_write(&cs40l26->dsp, &cs40l26->wseqs[CS40L26_WSEQ_POWER_ON], + CS40L26_BST_IPK_CTL, bst_ipk, CS_DSP_WSEQ_L16, true); +} + +static int cs40l26_bst_ctl_config(struct cs40l26 *cs40l26) +{ + u32 bst_ctl, bst_ctl_lim; + int ret; + + ret = regmap_read(cs40l26->regmap, CS40L26_VBST_CTL_2, &bst_ctl_lim); + if (ret) + return ret; + + bst_ctl_lim |= FIELD_PREP(CS40L26_BST_CTL_LIM_EN_MASK, CS40L26_BST_CTL_LIM_EN); + + ret = regmap_write(cs40l26->regmap, CS40L26_VBST_CTL_2, bst_ctl_lim); + if (ret) + return ret; + + ret = cs_dsp_wseq_write(&cs40l26->dsp, &cs40l26->wseqs[CS40L26_WSEQ_POWER_ON], + CS40L26_VBST_CTL_2, bst_ctl_lim, CS_DSP_WSEQ_L16, true); + if (ret) + return ret; + + bst_ctl = (clamp_val(cs40l26->vbst_uv, CS40L26_BST_UV_MIN, CS40L26_BST_UV_MAX) - + CS40L26_BST_UV_MIN) / CS40L26_BST_UV_STEP; + + ret = regmap_write(cs40l26->regmap, CS40L26_VBST_CTL_1, bst_ctl); + if (ret) + return ret; + + return cs_dsp_wseq_write(&cs40l26->dsp, &cs40l26->wseqs[CS40L26_WSEQ_POWER_ON], + CS40L26_VBST_CTL_1, bst_ctl, CS_DSP_WSEQ_L16, true); +} + +static int cs40l26_irq_init(struct cs40l26 *cs40l26) +{ + int i, ret; + + /* Unmask relevant warnings and error interrupts */ + for (i = CS40L26_IRQ_BST_OVP_FLAG_RISE; i <= CS40L26_IRQ_AMP_ERR; i++) + cs40l26_irq_unmask(cs40l26, 1, i); + + cs40l26_irq_unmask(cs40l26, 1, CS40L26_IRQ_DSP_RX_QUEUE); + + for (i = CS40L26_IRQ_VPMON_CLIPPED; i <= CS40L26_IRQ_IMON_CLIPPED; i++) + cs40l26_irq_unmask(cs40l26, 2, i); + + for (i = CS40L26_IRQ_REFCLK_PRESENT; i <= CS40L26_IRQ_REFCLK_MISSING_RISE; i++) + cs40l26_irq_unmask(cs40l26, 2, i); + + ret = regmap_multi_reg_write(cs40l26->regmap, cs40l26_irq_masks, 2); + if (ret) + return ret; + + ret = cs_dsp_wseq_multi_write(&cs40l26->dsp, &cs40l26->wseqs[CS40L26_WSEQ_POWER_ON], + cs40l26_irq_masks, 2, CS_DSP_WSEQ_FULL, true); + if (ret) + return ret; + + ret = devm_request_threaded_irq(cs40l26->dev, cs40l26->irq, NULL, cs40l26_irq_handler, + IRQF_ONESHOT, "cs40l26", cs40l26); + if (ret) + dev_err(cs40l26->dev, "Failed to request IRQ\n"); + + return ret; +} + +static int cs40l26_hw_init(struct cs40l26 *cs40l26) +{ + int ret; + + cs40l26->irq_masks = cs40l26_irq_masks; + + ret = cs40l26_wksrc_config(cs40l26); + if (ret) + return ret; + + cs40l26_gpio_config(cs40l26); + + ret = cs40l26_bst_ipk_config(cs40l26); + if (ret) + return ret; + + ret = cs40l26_bst_ctl_config(cs40l26); + if (ret) + return ret; + + return cs40l26_irq_init(cs40l26); +} + +static int cs40l26_cs_dsp_pre_run(struct cs_dsp *dsp) +{ + struct cs40l26 *cs40l26 = container_of(dsp, struct cs40l26, dsp); + int ret; + + ret = cs40l26_pm_timeout_ms_set(cs40l26, CS40L26_DSP_STATE_STANDBY, 100); + if (ret) { + dev_err(cs40l26->dev, "Failed to set standby timeout\n"); + return ret; + } + + ret = cs40l26_pm_timeout_ms_set(cs40l26, CS40L26_DSP_STATE_ACTIVE, 250); + if (ret) { + dev_err(cs40l26->dev, "Failed to set active timeout\n"); + return ret; + } + + ret = regmap_set_bits(cs40l26->regmap, CS40L26_PWRMGT_CTL, CS40L26_MEM_RDY); + if (ret) { + dev_err(cs40l26->dev, "Failed to set MEM_RDY\n"); + return ret; + } + + ret = cs40l26_fw_read(dsp, "QUEUE_BASE", CS40L26_DSP_ALGO_ID, &cs40l26->queue_base); + if (ret) + return ret; + + ret = cs40l26_fw_read(dsp, "QUEUE_LEN", CS40L26_DSP_ALGO_ID, &cs40l26->queue_len); + if (ret) + return ret; + + cs40l26->queue_last = cs40l26->queue_base + ((cs40l26->queue_len - 1) * sizeof(u32)); + + ret = cs40l26_fw_write(dsp, "CALL_RAM_INIT", dsp->fw_id, 1); + if (ret) + return ret; + + ret = cs40l26_wseq_init(cs40l26); + if (ret) + return ret; + + if (cs40l26->variant->handle_errata) + return cs40l26->variant->handle_errata(cs40l26); + else + return 0; +} + +static int cs40l26_cs_dsp_post_run(struct cs_dsp *dsp) +{ + struct cs40l26 *cs40l26 = container_of(dsp, struct cs40l26, dsp); + u32 halo_state; + int ret; + + /* + * cs_dsp_halo_start_core() has reset the DSP core at this point. + * Hibernation must be disabled again. + */ + ret = cs40l26_prevent_hiber(cs40l26); + if (ret) + return ret; + + ret = cs40l26_fw_read(dsp, "HALO_STATE", dsp->fw_id, &halo_state); + if (ret) + return ret; + + if (halo_state != CS40L26_DSP_HALO_STATE_RUN) { + dev_err(dsp->dev, "Invalid DSP state: %u\n", halo_state); + return -EINVAL; + } + + ret = cs40l26_hw_init(cs40l26); + if (ret) + return ret; + + dev_dbg(dsp->dev, "CS40L26/L27 DSP started successfully\n"); + + ret = devm_mfd_add_devices(cs40l26->dev, PLATFORM_DEVID_AUTO, cs40l26_devs, + ARRAY_SIZE(cs40l26_devs), NULL, 0, NULL); + if (ret) + dev_err(cs40l26->dev, "Failed to add MFD child devices: %d\n", ret); + + return ret; +} + +static const struct cs_dsp_client_ops cs40l26_cs_dsp_client_ops = { + .pre_run = cs40l26_cs_dsp_pre_run, + .post_run = cs40l26_cs_dsp_post_run, +}; + +static void cs40l26_cs_dsp_remove(void *data) +{ + cs_dsp_remove((struct cs_dsp *)data); +} + +static struct cs_dsp_coeff_desc cs40l26_coeffs[] = { + { .coeff_firmware = NULL, .coeff_filename = "cs40l26.bin" }, + { .coeff_firmware = NULL, .coeff_filename = "cs40l26-svc.bin" }, + { .coeff_firmware = NULL, .coeff_filename = "cs40l26-dvl.bin" }, +}; + +static int cs40l26_cs_dsp_init(struct cs40l26 *cs40l26) +{ + struct cs_dsp *dsp = &cs40l26->dsp; + int ret; + + dsp->num = 1; + dsp->type = WMFW_HALO; + dsp->dev = cs40l26->dev; + dsp->regmap = cs40l26->regmap; + dsp->base = CS40L26_DSP_CTRL_BASE; + dsp->base_sysinfo = CS40L26_DSP1_SYS_INFO_ID; + dsp->mem = cs40l26_dsp_regions; + dsp->num_mems = ARRAY_SIZE(cs40l26_dsp_regions); + dsp->client_ops = &cs40l26_cs_dsp_client_ops; + + ret = cs_dsp_halo_init(dsp); + if (ret) { + dev_err(cs40l26->dev, "Failed to initialize HALO core\n"); + return ret; + } + + return devm_add_action_or_reset(cs40l26->dev, cs40l26_cs_dsp_remove, dsp); +} + +static void cs40l26_dsp_start(struct cs40l26 *cs40l26) +{ + int i, ret; + + ret = cs40l26_dsp_pre_config(cs40l26); + if (ret) { + dev_err(cs40l26->dev, "DSP Pre Config. Failed: %d\n", ret); + goto err_fw_rls; + } + + guard(mutex)(&cs40l26->lock); + + ret = cs_dsp_power_up_multiple(&cs40l26->dsp, cs40l26->wmfw, "cs40l26.wmfw", cs40l26_coeffs, + CS40L26_NUM_COEFF_FILES, "cs40l26"); + if (ret) { + dev_err(cs40l26->dev, "Failed to Power Up DSP\n"); + goto err_fw_rls; + } + + if (cs40l26->dsp.fw_id != CS40L26_FW_ID) { + dev_err(cs40l26->dev, "Invalid firmware ID: 0x%X\n", cs40l26->dsp.fw_id); + goto err_fw_rls; + } + + if (cs40l26->dsp.fw_id_version < cs40l26->variant->info->fw_min_rev) { + dev_err(cs40l26->dev, "Invalid firmware revision: 0x%X\n", + cs40l26->dsp.fw_id_version); + goto err_fw_rls; + } + + ret = cs_dsp_run(&cs40l26->dsp); + if (ret) + dev_err(cs40l26->dev, "DSP Failed to run: %d\n", ret); + +err_fw_rls: + for (i = 0; i < CS40L26_NUM_COEFF_FILES; i++) + release_firmware(cs40l26_coeffs[i].coeff_firmware); + + release_firmware(cs40l26->wmfw); +} + +static void cs40l26_fw_upload(const struct firmware *wmfw, void *context) +{ + struct cs40l26 *cs40l26 = (struct cs40l26 *)context; + const struct firmware *coeff; + int i, ret; + + if (!wmfw) { + dev_err(cs40l26->dev, "Failed to request firmware file\n"); + return; + } + + cs40l26->wmfw = wmfw; + + for (i = 0; i < CS40L26_NUM_COEFF_FILES; i++) { + ret = request_firmware(&coeff, cs40l26_coeffs[i].coeff_filename, cs40l26->dev); + if (ret) + continue; + + cs40l26_coeffs[i].coeff_firmware = coeff; + } + + return cs40l26_dsp_start(cs40l26); +} + +static int cs40l26_init(struct cs40l26 *cs40l26) +{ + int ret; + + cs40l26->bst_ipk_ua = CS40L26_BST_IPK_UA_DEFAULT; + cs40l26->vbst_uv = CS40L26_BST_UV_MAX; + /* + * Set the PLL to open-loop and remove default GPI mappings to prevent DSP lockup while + * the driver configures RAM firmware. + * + * The firmware will set the PLL back to closed-loop when the DSP has been started. + */ + ret = cs40l26_set_pll_loop(cs40l26, CS40L26_PLL_OPEN); + if (ret) + return ret; + + ret = regmap_write(cs40l26->regmap, cs40l26->variant->info->event_map_1, + CS40L26_EVENT_MAP_GPI_DISABLE); + if (ret) + return ret; + + ret = regmap_write(cs40l26->regmap, cs40l26->variant->info->event_map_2, + CS40L26_EVENT_MAP_GPI_DISABLE); + if (ret) + return ret; + + /* Set LRA to HI-Z to avoid fault conditions */ + return regmap_set_bits(cs40l26->regmap, CS40L26_TST_DAC_MSM_CONFIG, + CS40L26_SPK_DEFAULT_HIZ); +} + +static int cs40l26_parse_properties(struct cs40l26 *cs40l26) +{ + struct device *dev = cs40l26->dev; + int ret; + + ret = device_property_read_u32(dev, "cirrus,bst-ctl-microvolt", &cs40l26->vbst_uv); + if (ret && ret != -EINVAL) + return ret; + + ret = device_property_read_u32(dev, "cirrus,bst-ipk-microamp", &cs40l26->bst_ipk_ua); + if (ret && ret != -EINVAL) + return ret; + + return 0; +} + +int cs40l26_probe(struct cs40l26 *cs40l26) +{ + int ret; + + mutex_init(&cs40l26->lock); + + cs40l26->reset_gpio = devm_gpiod_get_optional(cs40l26->dev, "reset", GPIOD_OUT_HIGH); + if (!cs40l26->reset_gpio) + return dev_err_probe(cs40l26->dev, -EINVAL, "Failed to get reset GPIO\n"); + + ret = devm_regulator_bulk_get_enable(cs40l26->dev, ARRAY_SIZE(cs40l26_supplies), + cs40l26_supplies); + if (ret) + return dev_err_probe(cs40l26->dev, ret, "Failed to get supplies\n"); + + usleep_range(CS40L26_MIN_RESET_PULSE_US, CS40L26_MIN_RESET_PULSE_US + 100); + + gpiod_set_value_cansleep(cs40l26->reset_gpio, 0); + + usleep_range(CS40L26_CP_READY_DELAY_US, CS40L26_CP_READY_DELAY_US + 100); + + ret = cs40l26_get_model(cs40l26); + if (ret) + return dev_err_probe(cs40l26->dev, ret, "Failed to get part number\n"); + + ret = cs40l26_prevent_hiber(cs40l26); + if (ret) + return dev_err_probe(cs40l26->dev, ret, "Failed to prevent hibernation\n"); + + ret = cs40l26_init(cs40l26); + if (ret) + return dev_err_probe(cs40l26->dev, ret, "Failed to initialize device\n"); + + ret = cs40l26_parse_properties(cs40l26); + if (ret) + return dev_err_probe(cs40l26->dev, ret, "Failed to parse devicetree\n"); + + ret = cs40l26_cs_dsp_init(cs40l26); + if (ret) + return dev_err_probe(cs40l26->dev, ret, "Failed to initialize CS DSP\n"); + + ret = cs40l26_pm_runtime_setup(cs40l26->dev); + if (ret) + return dev_err_probe(cs40l26->dev, ret, "Failed to set up PM Runtime\n"); + + ret = request_firmware_nowait(THIS_MODULE, FW_ACTION_UEVENT, "cs40l26.wmfw", cs40l26->dev, + GFP_KERNEL, cs40l26, cs40l26_fw_upload); + if (ret) + return dev_err_probe(cs40l26->dev, ret, "Failed to load firmware\n"); + + cs40l26_pm_exit(cs40l26->dev); + + return 0; +} +EXPORT_SYMBOL_GPL(cs40l26_probe); + +static int __maybe_unused cs40l26_suspend(struct device *dev) +{ + struct cs40l26 *cs40l26 = dev_get_drvdata(dev); + + guard(mutex)(&cs40l26->lock); + + dev_dbg(dev, "%s: Enabling hibernation\n", __func__); + + cs40l26->wksrc_sts = 0x00; + + /* Don't poll DSP since reading for ACK will wake the device again */ + return regmap_write(cs40l26->regmap, CS40L26_DSP_QUEUE, CS40L26_DSP_CMD_ALLOW_HIBER); +} + +static int __maybe_unused cs40l26_sys_suspend(struct device *dev) +{ + struct cs40l26 *cs40l26 = dev_get_drvdata(dev); + + dev_dbg(dev, "System suspend, disabling IRQ\n"); + + disable_irq(cs40l26->irq); + + return 0; +} + +static int __maybe_unused cs40l26_sys_suspend_noirq(struct device *dev) +{ + struct cs40l26 *cs40l26 = dev_get_drvdata(dev); + + dev_dbg(dev, "Late system suspend, re-enabling IRQ\n"); + + enable_irq(cs40l26->irq); + + return 0; +} + +static int __maybe_unused cs40l26_resume(struct device *dev) +{ + struct cs40l26 *cs40l26 = dev_get_drvdata(dev); + + dev_dbg(dev, "%s: Disabling hibernation\n", __func__); + + guard(mutex)(&cs40l26->dsp.pwr_lock); + + return cs40l26_prevent_hiber(cs40l26); +} + +static int __maybe_unused cs40l26_sys_resume(struct device *dev) +{ + struct cs40l26 *cs40l26 = dev_get_drvdata(dev); + + dev_dbg(dev, "System resume, re-enabling IRQ\n"); + + enable_irq(cs40l26->irq); + + return 0; +} + +static int __maybe_unused cs40l26_sys_resume_noirq(struct device *dev) +{ + struct cs40l26 *cs40l26 = dev_get_drvdata(dev); + + dev_dbg(dev, "Early system resume, disabling IRQ\n"); + + disable_irq(cs40l26->irq); + + return 0; +} + +EXPORT_GPL_DEV_PM_OPS(cs40l26_pm_ops) = { + RUNTIME_PM_OPS(cs40l26_suspend, cs40l26_resume, NULL) + SYSTEM_SLEEP_PM_OPS(cs40l26_sys_suspend, cs40l26_sys_resume) + NOIRQ_SYSTEM_SLEEP_PM_OPS(cs40l26_sys_suspend_noirq, cs40l26_sys_resume_noirq) +}; + +MODULE_DESCRIPTION("CS40L26 Boosted Class D Amplifier for Haptics"); +MODULE_AUTHOR("Fred Treven, Cirrus Logic Inc. "); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("FW_CS_DSP"); diff --git a/drivers/mfd/cs40l26-i2c.c b/drivers/mfd/cs40l26-i2c.c new file mode 100644 index 000000000000..c6e4118775a2 --- /dev/null +++ b/drivers/mfd/cs40l26-i2c.c @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * CS40L26 Boosted Haptic Driver with Integrated DSP and + * Waveform Memory with Advanced Closed Loop Algorithms and LRA protection + * + * Copyright 2025 Cirrus Logic, Inc. + * + * Author: Fred Treven + */ + +#include +#include + +static int cs40l26_i2c_probe(struct i2c_client *i2c) +{ + struct cs40l26 *cs40l26; + + cs40l26 = devm_kzalloc(&i2c->dev, sizeof(struct cs40l26), GFP_KERNEL); + if (!cs40l26) + return -ENOMEM; + + i2c_set_clientdata(i2c, cs40l26); + + cs40l26->dev = &i2c->dev; + cs40l26->irq = i2c->irq; + cs40l26->bus = &i2c_bus_type; + + cs40l26->regmap = devm_regmap_init_i2c(i2c, &cs40l26_regmap); + if (IS_ERR(cs40l26->regmap)) + return dev_err_probe(cs40l26->dev, PTR_ERR(cs40l26->regmap), + "Failed to allocate register map\n"); + + return cs40l26_probe(cs40l26); +} + +static const struct i2c_device_id cs40l26_id_i2c[] = { + { "cs40l26a", 0 }, + { "cs40l27b", 1 }, + {} +}; +MODULE_DEVICE_TABLE(i2c, cs40l26_id_i2c); + +static const struct of_device_id cs40l26_of_match[] = { + { .compatible = "cirrus,cs40l26a" }, + { .compatible = "cirrus,cs40l27b" }, + {} +}; +MODULE_DEVICE_TABLE(of, cs40l26_of_match); + +static struct i2c_driver cs40l26_i2c_driver = { + .driver = { + .name = "cs40l26", + .of_match_table = cs40l26_of_match, + .pm = pm_ptr(&cs40l26_pm_ops), + }, + .id_table = cs40l26_id_i2c, + .probe = cs40l26_i2c_probe, +}; +module_i2c_driver(cs40l26_i2c_driver); + +MODULE_DESCRIPTION("CS40L26 I2C Driver"); +MODULE_AUTHOR("Fred Treven, Cirrus Logic Inc. "); +MODULE_LICENSE("GPL"); diff --git a/drivers/mfd/cs40l26-spi.c b/drivers/mfd/cs40l26-spi.c new file mode 100644 index 000000000000..57fc92356d9d --- /dev/null +++ b/drivers/mfd/cs40l26-spi.c @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * CS40L26 Boosted Haptic Driver with Integrated DSP and + * Waveform Memory with Advanced Closed Loop Algorithms and LRA protection + * + * Copyright 2025 Cirrus Logic, Inc. + * + * Author: Fred Treven + */ + +#include +#include + +static int cs40l26_spi_probe(struct spi_device *spi) +{ + struct cs40l26 *cs40l26; + + cs40l26 = devm_kzalloc(&spi->dev, sizeof(struct cs40l26), GFP_KERNEL); + if (!cs40l26) + return -ENOMEM; + + spi_set_drvdata(spi, cs40l26); + + cs40l26->dev = &spi->dev; + cs40l26->irq = spi->irq; + cs40l26->bus = &spi_bus_type; + + cs40l26->regmap = devm_regmap_init_spi(spi, &cs40l26_regmap); + if (IS_ERR(cs40l26->regmap)) + return dev_err_probe(cs40l26->dev, PTR_ERR(cs40l26->regmap), + "Failed to allocate register map\n"); + + return cs40l26_probe(cs40l26); +} + +static const struct spi_device_id cs40l26_id_spi[] = { + { "cs40l26a", 0 }, + { "cs40l27b", 1 }, + {} +}; +MODULE_DEVICE_TABLE(spi, cs40l26_id_spi); + +static const struct of_device_id cs40l26_of_match[] = { + { .compatible = "cirrus,cs40l26a" }, + { .compatible = "cirrus,cs40l27b" }, + {} +}; +MODULE_DEVICE_TABLE(of, cs40l26_of_match); + +static struct spi_driver cs40l26_spi_driver = { + .driver = { + .name = "cs40l26", + .of_match_table = cs40l26_of_match, + .pm = pm_ptr(&cs40l26_pm_ops), + }, + .id_table = cs40l26_id_spi, + .probe = cs40l26_spi_probe, +}; +module_spi_driver(cs40l26_spi_driver); + +MODULE_DESCRIPTION("CS40L26 SPI Driver"); +MODULE_AUTHOR("Fred Treven, Cirrus Logic Inc. "); +MODULE_LICENSE("GPL"); diff --git a/include/linux/mfd/cs40l26.h b/include/linux/mfd/cs40l26.h new file mode 100644 index 000000000000..c0647c09e24d --- /dev/null +++ b/include/linux/mfd/cs40l26.h @@ -0,0 +1,341 @@ +/* SPDX-License-Identifier: GPL-2.0 + * + * CS40L26 Boosted Haptic Driver with Integrated DSP and + * Waveform Memory with Advanced Closed Loop Algorithms and LRA protection + * + * Copyright 2025 Cirrus Logic, Inc. + * + * Author: Fred Treven + */ + +#ifndef __MFD_CS40L26_H__ +#define __MFD_CS40L26_H__ + +#include +#include +#include +#include +#include +#include +#include +#include + +/* Register Addresses */ +#define CS40L26_LASTREG 0x3C7DFE8 +#define CS40L26_DEVID 0x0 +#define CS40L26_REVID 0x4 +#define CS40L26_GLOBAL_ENABLES 0x2014 +#define CS40L26_ERROR_RELEASE 0x2034 +#define CS40L26_PWRMGT_CTL 0x2900 +#define CS40L26_WAKESRC_CTL 0x2904 +#define CS40L26_PWRMGT_STS 0x290C +#define CS40L26_REFCLK_INPUT 0x2C04 +#define CS40L26_PLL_REFCLK_DETECT_0 0x2C28 +#define CS40L26_VBST_CTL_1 0x3800 +#define CS40L26_VBST_CTL_2 0x3804 +#define CS40L26_BST_IPK_CTL 0x3808 +#define CS40L26_TEST_LBST 0x391C +#define CS40L26_DAC_MSM_CONFIG 0x7400 +#define CS40L26_TST_DAC_MSM_CONFIG 0x7404 +#define CS40L26_IRQ1_STATUS 0x10004 +#define CS40L26_IRQ1_EINT_1 0x10010 +#define CS40L26_IRQ1_EINT_2 0x10014 +#define CS40L26_IRQ1_MASK_1 0x10110 +#define CS40L26_IRQ1_MASK_2 0x10114 +#define CS40L26_DSP_QUEUE 0x13020 +#define CS40L26_DSP1_XMEM_PACKED_0 0x2000000 +#define CS40L26_DSP1_SYS_INFO_ID 0x25E0000 +#define CS40L26_DSP1_XMEM_UNPACKED24_0 0x2800000 +#define CS40L26_DSP1_CCM_CORE_CONTROL 0x2BC1000 +#define CS40L26_DSP1_YMEM_PACKED_0 0x2C00000 +#define CS40L26_DSP1_YMEM_UNPACKED32_0 0x3000000 +#define CS40L26_DSP1_YMEM_UNPACKED24_0 0x3400000 +#define CS40L26_DSP1_PMEM_0 0x3800000 + +/* Device */ +#define CS40L26_DEVID_L26 0x40A260 +#define CS40L26_DEVID_L27 0x40A270 +#define CS40L26_REVID_A1 0xA1 +#define CS40L26_REVID_B1 0xB1 +#define CS40L26_REVID_B2 0xB2 +#define CS40L26_MIN_RESET_PULSE_US 1500 +#define CS40L26_CP_READY_DELAY_US 6000 +#define CS40L26_SPK_DEFAULT_HIZ BIT(28) +#define CS40L26_DSP_CCM_CORE_KILL 0x00000080 +#define CS40L26_MEM_RDY BIT(1) + +/* Errata */ +#define CS40L26_DISABLE_EXPL_MODE 0x0100C080 + +#define CS40L26_PLL_REFCLK_DET_DISABLE 0x0 + +/* Boost Converter Control */ +#define CS40L26_GLOBAL_EN BIT(0) + +#define CS40L26_BST_IPK_UA_MAX 4800000 +#define CS40L26_BST_IPK_UA_DEFAULT 4500000 +#define CS40L26_BST_IPK_UA_MIN 1600000 +#define CS40L26_BST_IPK_UA_STEP 50000 +#define CS40L26_BST_IPK_UA_OFFSET 800000 + +#define CS40L26_BST_UV_MIN 2500000 +#define CS40L26_BST_UV_MAX 11000000 +#define CS40L26_BST_UV_STEP 50000 + +#define CS40L26_BST_CTL_VP 0x00 +#define CS40L26_BST_CTL_MASK GENMASK(7, 0) +#define CS40L26_BST_CTL_SEL_MASK GENMASK(1, 0) +#define CS40L26_BST_CTL_SEL_FIXED 0x0 +#define CS40L26_BST_CTL_LIM_EN_MASK BIT(2) +#define CS40L26_BST_CTL_LIM_EN 1 + +#define CS40L26_BST_TIME_US 10000 + +/* Phase Locked Loop */ +#define CS40L26_PLL_REFCLK_LOOP_MASK BIT(11) +#define CS40L26_PLL_REFCLK_LOOP_SHIFT 11 +#define CS40L26_PLL_NUM_SET_ATTEMPTS 5 + +/* GPIO */ +#define CS40L26_EVENT_MAP_GPI_DISABLE 0x1FF + +#define CS40L26_A1_B1_EVENT_MAP_1 0x02806FC4 +#define CS40L26_A1_B1_EVENT_MAP_2 0x02806FC8 + +#define CS40L26_B2_EVENT_MAP_1 0x02806FB0 +#define CS40L26_B2_EVENT_MAP_2 0x02806FB4 + +/* Power Management */ +#define CS40L26_PM_STDBY_TICKS_OFFSET 16 +#define CS40L26_PM_ACTIVE_TICKS_OFFSET 24 + +#define CS40L26_A1_B1_PM_CUR_STATE 0x02800370 +#define CS40L26_A1_B1_PM_STATE_LOCKS 0x02800378 +#define CS40L26_A1_B1_PM_STATE_LOCKS3 (CS40L26_A1_B1_PM_STATE_LOCKS + \ + CS40L26_DSP_LOCK3_OFFSET_BYTES) + +#define CS40L26_A1_B1_PM_TIMEOUT_TICKS 0x02800350 +#define CS40L26_A1_B1_PM_STDBY_TICKS (CS40L26_A1_B1_PM_TIMEOUT_TICKS + \ + CS40L26_PM_STDBY_TICKS_OFFSET) +#define CS40L26_A1_B1_PM_ACTIVE_TICKS (CS40L26_A1_B1_PM_TIMEOUT_TICKS + \ + CS40L26_PM_ACTIVE_TICKS_OFFSET) + +#define CS40L26_A1_B1_HALO_STATE 0x02800FA8 + +#define CS40L26_B2_PM_CUR_STATE 0x02801F98 +#define CS40L26_B2_PM_STATE_LOCKS 0x02801FA0 +#define CS40L26_B2_PM_STATE_LOCKS3 (CS40L26_B2_PM_STATE_LOCKS + CS40L26_DSP_LOCK3_OFFSET_BYTES) +#define CS40L26_B2_PM_TIMEOUT_TICKS 0x02801F78 +#define CS40L26_B2_PM_STDBY_TICKS (CS40L26_B2_PM_TIMEOUT_TICKS + \ + CS40L26_PM_STDBY_TICKS_OFFSET) +#define CS40L26_B2_PM_ACTIVE_TICKS (CS40L26_B2_PM_TIMEOUT_TICKS + \ + CS40L26_PM_ACTIVE_TICKS_OFFSET) + +#define CS40L26_B2_HALO_STATE 0x02806AF8 + +#define CS40L26_AUTOSUSPEND_DELAY_MS 2000 +#define CS40L26_PM_TICKS_PER_SEC 32768 + +/* Firmware Handling */ +#define CS40L26_FW_ID 0x1800D4 +#define CS40L26_FW_A1_B1_MIN_REV 0x070247 +#define CS40L26_FW_B2_MIN_REV 0x0A0000 + +#define CS40L26_NUM_COEFF_FILES 3 + +/* Algorithms */ +#define CS40L26_VIBEGEN_ALGO_ID_A1 0x000400BD +#define CS40L26_VIBEGEN_ALGO_ID_B2 0x000A00BD + +#define CS40L26_BUZZGEN_ALGO_ID 0x0004F202 +#define CS40L26_A2H_ALGO_ID 0x00040110 +#define CS40L26_EXT_ALGO_ID 0x0004013C +#define CS40L26_DSP_ALGO_ID 0x0004F203 +#define CS40L26_PM_ALGO_ID 0x0004F206 + +/* DSP */ +#define CS40L26_DSP_LOCK3_OFFSET_BYTES 8 +#define CS40L26_DSP_LOCK3_OFFSET_WORDS (CS40L26_DSP_LOCK3_OFFSET_BYTES / sizeof(u32)) +#define CS40L26_DSP_LOCK3_MASK BIT(1) +#define CS40L26_DSP_HALO_STATE_RUN 2 +#define CS40L26_DSP_CTRL_BASE 0x2B80000 +#define CS40L26_DSP_POLL_US 1000 +#define CS40L26_DSP_TIMEOUT_COUNT 100 +#define CS40L26_PM_LOCKS_TIMEOUT_COUNT 10 +#define CS40L26_DSP_STATE_TIMEOUT_COUNT 10 + +#define CS40L26_DSP_CMD_PREVENT_HIBER 0x02000003 +#define CS40L26_DSP_CMD_ALLOW_HIBER 0x02000004 +#define CS40L26_DSP_CMD_INDEX_MASK GENMASK(28, 24) +#define CS40L26_DSP_CMD_PAYLOAD_MASK GENMASK(23, 0) + +#define CS40L26_DSP_COMPLETE_CP 0x01000000 +#define CS40L26_DSP_COMPLETE_I2S 0x01000002 +#define CS40L26_DSP_TRIGGER_CP 0x01000010 +#define CS40L26_DSP_TRIGGER_I2S 0x01000012 +#define CS40L26_DSP_PM_AWAKE 0x02000002 +#define CS40L26_DSP_SYS_ACK 0x0A000000 +#define CS40L26_DSP_PANIC 0x0C000000 + +/* Wake Sources */ +#define CS40L26_WKSRC_STS_MASK GENMASK(9, 4) +#define CS40L26_WKSRC_STS_SHIFT 4 +#define CS40L26_WKSRC_STS_EN BIT(7) +#define CS40L26_WKSRC_POL_SPI BIT(4) +#define CS40L26_WKSRC_EN_SPI BIT(9) +#define CS40L26_WKSRC_EN_I2C BIT(10) +#define CS40L26_WKSRC_GPIO_POL_MASK GENMASK(3, 0) + +/* Interrupts */ +#define CS40L26_IRQ_GPIO1_RISE 0 +#define CS40L26_IRQ_GPIO1_FALL 1 +#define CS40L26_IRQ_WKSRC_STS_ANY 8 +#define CS40L26_IRQ_WKSRC_STS_GPIO1 9 +#define CS40L26_IRQ_WKSRC_STS_SPI 13 +#define CS40L26_IRQ_WKSRC_STS_I2C 14 +#define CS40L26_IRQ_BST_OVP_FLAG_RISE 18 +#define CS40L26_IRQ_BST_OVP_FLAG_FALL 19 +#define CS40L26_IRQ_BST_OVP_ERR 20 +#define CS40L26_IRQ_BST_DCM_UVP_ERR 21 +#define CS40L26_IRQ_BST_SHORT_ERR 22 +#define CS40L26_IRQ_BST_IPK_FLAG 23 +#define CS40L26_IRQ_TEMP_WARN_RISE 24 +#define CS40L26_IRQ_TEMP_WARN_FALL 25 +#define CS40L26_IRQ_TEMP_ERR 26 +#define CS40L26_IRQ_AMP_ERR 27 +#define CS40L26_IRQ_DSP_RX_QUEUE 31 + +#define CS40L26_IRQ_1_NBITS 32 + +#define CS40L26_IRQ_REFCLK_PRESENT 6 +#define CS40L26_IRQ_REFCLK_MISSING_FALL 7 +#define CS40L26_IRQ_REFCLK_MISSING_RISE 8 +#define CS40L26_IRQ_VPMON_CLIPPED 23 +#define CS40L26_IRQ_VBSTMON_CLIPPED 24 +#define CS40L26_IRQ_VMON_CLIPPED 25 +#define CS40L26_IRQ_IMON_CLIPPED 26 + +#define CS40L26_IRQ_2_NBITS 30 + +#define CS40L26_IRQ_1_ALL_MASKED 0xFFFFFFFF +#define CS40L26_IRQ_2_ALL_MASKED 0x3FFFFFFF + +#define CS40L26_IRQ_STATUS_ASSERT 0x1 + +/* Playback */ +#define CS40L26_STOP_PLAYBACK 0x05000000 + +#define CS40L26_START_I2S 0x03000002 +#define CS40L26_STOP_I2S 0x03000003 + +/* Error Release */ +enum cs40l26_error { + CS40L26_ERROR_NONE, + CS40L26_ERROR_AMP_SHORT, + CS40L26_ERROR_BST_SHORT, + CS40L26_ERROR_BST_OVP, + CS40L26_ERROR_BST_DCM_UVP, + CS40L26_ERROR_TEMP_WARN, + CS40L26_ERROR_TEMP_ERR, +}; + +struct cs40l26_irq { + int virq; + u32 mask; + const char *name; + int (*handler)(void *data); +}; + +#define CS40L26_IRQ(_irq, _name, _hand) \ + { \ + .virq = CS40L26_IRQ_ ## _irq, \ + .mask = BIT(CS40L26_ ## IRQ_ ## _irq), \ + .name = _name, \ + .handler = _hand, \ + } + +enum cs40l26_dsp_state { + CS40L26_DSP_STATE_HIBERNATE, + CS40L26_DSP_STATE_SHUTDOWN, + CS40L26_DSP_STATE_STANDBY, + CS40L26_DSP_STATE_ACTIVE, + CS40L26_DSP_STATE_NONE, +}; + +enum cs40l26_gpio_map { + CS40L26_GPIO_MAP_A_PRESS, + CS40L26_GPIO_MAP_A_RELEASE, + CS40L26_GPIO_MAP_NUM_AVAILABLE, + CS40L26_GPIO_MAP_INVALID, +}; + +enum cs40l26_pll { + CS40L26_PLL_CLOSED, + CS40L26_PLL_OPEN, +}; + +enum cs40l50_wseqs { + CS40L26_WSEQ_POWER_ON, + CS40L26_WSEQ_ACTIVE, + CS40L26_WSEQ_STANDBY, + CS40L26_NUM_WSEQS, +}; + +struct cs40l26_variant_info { + u32 pm_cur_state; + u32 pm_state_locks; + u32 pm_state_locks3; + u32 pm_stdby_ticks; + u32 pm_active_ticks; + u32 halo_state; + u32 event_map_1; + u32 event_map_2; + u32 fw_min_rev; + u32 ram_ext_algo_id; + u32 vibegen_algo_id; +}; + +struct cs40l26_variant; + +struct cs40l26 { + struct device *dev; + struct regmap *regmap; + struct cs_dsp dsp; + int irq; + struct mutex lock; + struct gpio_desc *reset_gpio; + u32 devid; + u32 revid; + const struct cs40l26_variant *variant; + struct cs_dsp_wseq wseqs[CS40L26_NUM_WSEQS]; + u8 wksrc_sts; + u8 last_wksrc_pol; + u32 queue_base; + u32 queue_len; + u32 queue_last; + unsigned int bst_ipk_ua; + unsigned int vbst_uv; + const struct firmware *wmfw; + const struct bus_type *bus; + struct reg_sequence *irq_masks; +}; + +struct cs40l26_variant { + const struct cs40l26_variant_info *info; + int (*handle_errata)(struct cs40l26 *cs40l26); +}; + +inline void cs40l26_pm_exit(struct device *dev); +int cs40l26_probe(struct cs40l26 *cs40l26); +int cs40l26_set_pll_loop(struct cs40l26 *cs40l26, const u32 pll_loop); +int cs40l26_dsp_write(struct cs40l26 *cs40l26, const u32 val); +int cs40l26_dsp_state_get(struct cs40l26 *cs40l26, u32 *state); +inline int cs40l26_fw_write(struct cs_dsp *dsp, const char *const name, + const unsigned int algo_id, u32 val); +inline int cs40l26_fw_read(struct cs_dsp *dsp, const char *const name, + const unsigned int algo_id, u32 *buf); + +extern const struct regmap_config cs40l26_regmap; +extern const struct dev_pm_ops cs40l26_pm_ops; + +#endif /* __CS40L26_H__ */ From patchwork Tue Feb 4 23:18:35 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Fred Treven X-Patchwork-Id: 13960212 Received: from mx0b-001ae601.pphosted.com (mx0b-001ae601.pphosted.com [67.231.152.168]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id A0B6621C9EE; Tue, 4 Feb 2025 23:24:22 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=67.231.152.168 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1738711464; cv=none; b=kRs49NAXQKvunPI5PXs2K0Ab/DyH5tlP+bEl+t+f05+otqgLSstjmx9WXit8o65a6wcj7Gxx0Yk26uGeRVsdeKNsBNqrFZvmiqv9+dL3lpq9Sx4bLJ39a23+XORjjEaOzqCc7q8nJ6SwUK1SSIPpNgPyAIypFftNHMjlMCJiGYE= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1738711464; c=relaxed/simple; bh=sIln//XUWOCbv+jbIU+HIsNsEaNVSCpgYZ+FUr6PS8M=; h=From:To:CC:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=Hg1t9IzOqyeYLtlXihshKrawWMgvGHalNbml6caktxC3vt4wefl/aUcAz+q77WjH641bx8LhmpZ8Zwd7T+o9KFVVN5OAPdIXD9jnMddPPbkSb8bBwbVnw5E8uspuFbuzFAUqu+42aXpO9hGoZxqhgWVa4Y54Bd4q/nL/oR4L7jY= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=opensource.cirrus.com; spf=pass smtp.mailfrom=opensource.cirrus.com; dkim=pass (2048-bit key) header.d=cirrus.com header.i=@cirrus.com header.b=l+kysFiy; arc=none smtp.client-ip=67.231.152.168 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=opensource.cirrus.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=opensource.cirrus.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=cirrus.com header.i=@cirrus.com header.b="l+kysFiy" Received: from pps.filterd (m0077474.ppops.net [127.0.0.1]) by mx0b-001ae601.pphosted.com (8.18.1.2/8.18.1.2) with ESMTP id 514FmAYv022131; Tue, 4 Feb 2025 17:20:21 -0600 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cirrus.com; h=cc :content-transfer-encoding:content-type:date:from:in-reply-to :message-id:mime-version:references:subject:to; s= PODMain02222019; bh=vbeaKiAV8j9TomN/Mh6JI2DXEEZVRCZpyRAxRLZvhJo=; b= l+kysFiyTFiepxtfC6QC6cvJ8HYwcd0ZrZWifK86VUUIH7Ru0k+PPuHnVFg6Epmr 4Xv1OrwFiv6DuYieJB/Fj45RUVBxPdEkbw9hb88sVkG4Auy+XHkzN7LbrK5FZBpd xcStX6aFgr1Ap96ONvHW/W7POcAbVcL7XBjzArYnecwpxjxSoep7G6egLNvMLU5t 2rQR/SmK90lRNnPTGYi2tsOx/ma+yVDC7ae5VbnctM+vrX8WdMfMPKadZr831IoY eFf98m6B8/zRDNKVONV9jH2avFHyUfB5bdk2tUBBHetNL0jZAX8v7/lYnzoGH6tO 4dXXM91+E3ev12GPN75odw== Received: from ediex02.ad.cirrus.com ([84.19.233.68]) by mx0b-001ae601.pphosted.com (PPS) with ESMTPS id 44hgwm3sqg-1 (version=TLSv1.2 cipher=ECDHE-RSA-AES256-GCM-SHA384 bits=256 verify=NOT); Tue, 04 Feb 2025 17:20:21 -0600 (CST) Received: from ediex01.ad.cirrus.com (198.61.84.80) by ediex02.ad.cirrus.com (198.61.84.81) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.2.1544.14; Tue, 4 Feb 2025 23:20:19 +0000 Received: from ediswmail9.ad.cirrus.com (198.61.86.93) by anon-ediex01.ad.cirrus.com (198.61.84.80) with Microsoft SMTP Server id 15.2.1544.14 via Frontend Transport; Tue, 4 Feb 2025 23:20:14 +0000 Received: from ftrev.crystal.cirrus.com (ftrev.ad.cirrus.com [141.131.145.81]) by ediswmail9.ad.cirrus.com (Postfix) with ESMTP id 30309820248; Tue, 4 Feb 2025 23:20:10 +0000 (UTC) From: Fred Treven To: Lee Jones , Rob Herring , "Krzysztof Kozlowski" , Conor Dooley , "Simon Trimmer" , Charles Keepax , Richard Fitzgerald , Dmitry Torokhov , James Ogletree , Ben Bright , Liam Girdwood , Mark Brown , Jaroslav Kysela , Takashi Iwai , David Rhodes , Jeff LaBundy , Heiko Stuebner , Karel Balej , Igor Prusov , Jack Yu , Weidong Wang , Binbin Zhou , Prasad Kumpatla , "Paul Handrigan" , Masahiro Yamada , Nuno Sa , Fred Treven CC: , , , , , Subject: [PATCH RESEND 6/7] ASoC: cs40l26: Support I2S streaming to CS40L26 Date: Tue, 4 Feb 2025 17:18:35 -0600 Message-ID: <20250204231835.2000457-7-ftreven@opensource.cirrus.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20250204231835.2000457-1-ftreven@opensource.cirrus.com> References: <20250204231835.2000457-1-ftreven@opensource.cirrus.com> Precedence: bulk X-Mailing-List: linux-input@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Authority-Analysis: v=2.4 cv=EPv800ZC c=1 sm=1 tr=0 ts=67a2a0b5 cx=c_pps a=uGhh+3tQvKmCLpEUO+DX4w==:117 a=uGhh+3tQvKmCLpEUO+DX4w==:17 a=T2h4t0Lz3GQA:10 a=w1d2syhTAAAA:8 a=CNfp1fVX68x8Q3iYfccA:9 a=YXXWInSmI4Sqt1AkVdoW:22 X-Proofpoint-ORIG-GUID: lA0UnrLd6glWyAKyuQOnA1OYayb_B1pb X-Proofpoint-GUID: lA0UnrLd6glWyAKyuQOnA1OYayb_B1pb X-Proofpoint-Spam-Reason: safe Introduce codec support for Cirrus Logic Device CS40L26. The ASoC driver enables I2S streaming to the device. Signed-off-by: Fred Treven --- sound/soc/codecs/Kconfig | 12 + sound/soc/codecs/Makefile | 2 + sound/soc/codecs/cs40l26-codec.c | 523 +++++++++++++++++++++++++++++++ 3 files changed, 537 insertions(+) create mode 100644 sound/soc/codecs/cs40l26-codec.c diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig index ee35f3aa5521..850b5fab984c 100644 --- a/sound/soc/codecs/Kconfig +++ b/sound/soc/codecs/Kconfig @@ -77,6 +77,7 @@ config SND_SOC_ALL_CODECS imply SND_SOC_CS35L56_I2C imply SND_SOC_CS35L56_SPI imply SND_SOC_CS35L56_SDW + imply SND_SOC_CS40L26 imply SND_SOC_CS40L50 imply SND_SOC_CS42L42 imply SND_SOC_CS42L42_SDW @@ -875,6 +876,17 @@ config SND_SOC_CS35L56_SDW help Enable support for Cirrus Logic CS35L56 boosted amplifier with SoundWire control +config SND_SOC_CS40L26 + tristate "Cirrus Logic CS40L26 CODEC" + depends on MFD_CS40L26_CORE + help + This option enables support for I2S streaming to Cirrus Logic CS40L26. + + CS40L26 is a boosted haptic driver with integrated DSP and waveform + memory with advanced closed loop algorithms and LRA protection. + + If built as a module, it will be named snd-soc-cs40l26. + config SND_SOC_CS40L50 tristate "Cirrus Logic CS40L50 CODEC" depends on MFD_CS40L50_CORE diff --git a/sound/soc/codecs/Makefile b/sound/soc/codecs/Makefile index d7ad795603c1..086e18964e60 100644 --- a/sound/soc/codecs/Makefile +++ b/sound/soc/codecs/Makefile @@ -80,6 +80,7 @@ snd-soc-cs35l56-shared-y := cs35l56-shared.o snd-soc-cs35l56-i2c-y := cs35l56-i2c.o snd-soc-cs35l56-spi-y := cs35l56-spi.o snd-soc-cs35l56-sdw-y := cs35l56-sdw.o +snd-soc-cs40l26-y := cs40l26-codec.o snd-soc-cs40l50-y := cs40l50-codec.o snd-soc-cs42l42-y := cs42l42.o snd-soc-cs42l42-i2c-y := cs42l42-i2c.o @@ -497,6 +498,7 @@ obj-$(CONFIG_SND_SOC_CS35L56_SHARED) += snd-soc-cs35l56-shared.o obj-$(CONFIG_SND_SOC_CS35L56_I2C) += snd-soc-cs35l56-i2c.o obj-$(CONFIG_SND_SOC_CS35L56_SPI) += snd-soc-cs35l56-spi.o obj-$(CONFIG_SND_SOC_CS35L56_SDW) += snd-soc-cs35l56-sdw.o +obj-$(CONFIG_SND_SOC_CS40L26) += snd-soc-cs40l26.o obj-$(CONFIG_SND_SOC_CS40L50) += snd-soc-cs40l50.o obj-$(CONFIG_SND_SOC_CS42L42_CORE) += snd-soc-cs42l42.o obj-$(CONFIG_SND_SOC_CS42L42) += snd-soc-cs42l42-i2c.o diff --git a/sound/soc/codecs/cs40l26-codec.c b/sound/soc/codecs/cs40l26-codec.c new file mode 100644 index 000000000000..5bfaff0683a5 --- /dev/null +++ b/sound/soc/codecs/cs40l26-codec.c @@ -0,0 +1,523 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// CS40L26 Boosted Haptic Driver with integrated DSP and +// waveform memory with advanced closed loop algorithms and +// LRA protection +// +// Copyright 2025 Cirrus Logic Inc. +// +// Author: Fred Treven + +#include +#include +#include +#include + +#define CS40L26_MONITOR_FILT 0x4008 +#define CS40L26_ASP_ENABLES1 0x4800 +#define CS40L26_ASP_CONTROL2 0x4808 +#define CS40L26_ASP_FRAME_CONTROL5 0x4820 +#define CS40L26_ASP_DATA_CONTROL5 0x4840 +#define CS40L26_DACPCM1_INPUT 0x4C00 +#define CS40L26_ASPTX1_INPUT 0x4C20 + +#define CS40L26_PLL_CLK_SEL_BCLK 0x0 +#define CS40L26_PLL_CLK_SEL_MCLK 0x5 + +#define CS40L26_PLL_CLK_FREQ_MASK GENMASK(31, 0) + +#define CS40L26_FORMATS (SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE) +#define CS40L26_RATES (SNDRV_PCM_RATE_48000 | SNDRV_PCM_RATE_96000) + +#define CS40L26_ASP_RX_WIDTH_MASK GENMASK(31, 24) +#define CS40L26_ASP_FMT_MASK GENMASK(10, 8) +#define CS40L26_ASP_BCLK_INV_MASK BIT(6) +#define CS40L26_ASP_FSYNC_INV_MASK BIT(2) +#define CS40L26_ASP_FSYNC_INV_SHIFT 2 + +#define CS40L26_ASP_FMT_TDM1_DSPA 0x0 +#define CS40L26_ASP_FMT_I2S 0x2 + +#define CS40L26_PLL_REFCLK_BCLK 0x0 +#define CS40L26_PLL_REFCLK_FSYNC 0x1 +#define CS40L26_PLL_REFCLK_MCLK 0x5 + +#define CS40L26_PLL_REFCLK_SEL_MASK GENMASK(2, 0) +#define CS40L26_PLL_REFCLK_FREQ_MASK GENMASK(10, 5) +#define CS40L26_PLL_REFCLK_FREQ_SHIFT 5 +#define CS40L26_PLL_REFCLK_LOOP_MASK BIT(11) + +#define CS40L26_ASP_RX_WL_MASK GENMASK(5, 0) + +#define CS40L26_DATA_SRC_DSP1TX1 0x32 + +#define CS40L26_DATA_SRC_MASK GENMASK(6, 0) + +#define CS40L26_ASP_TX1_EN_MASK BIT(0) +#define CS40L26_ASP_TX2_EN_MASK BIT(1) +#define CS40L26_ASP_RX1_EN_MASK BIT(16) +#define CS40L26_ASP_RX2_EN_MASK BIT(17) +#define CS40L26_ASP_ENABLE_MASK \ + (CS40L26_ASP_TX1_EN_MASK | CS40L26_ASP_TX2_EN_MASK | CS40L26_ASP_RX1_EN_MASK | \ + CS40L26_ASP_RX2_EN_MASK) + +#define CS40L26_ASP_RX1_SLOT_MASK GENMASK(5, 0) +#define CS40L26_ASP_RX2_SLOT_MASK GENMASK(13, 8) + +#define CS40L26_VIMON_DUAL_RATE_MASK BIT(16) + +struct cs40l26_pll_sysclk_config { + u32 freq; + u8 cfg; +}; + +struct cs40l26_codec { + struct cs40l26 *core; + struct device *dev; + struct regmap *regmap; + unsigned int rate; + u32 daifmt; + int tdm_width; + int tdm_slot[2]; + u32 refclk_input; +}; + +static const struct cs40l26_pll_sysclk_config cs40l26_pll_sysclk[] = { + { 32768, 0x00 }, + { 1536000, 0x1B }, + { 3072000, 0x21 }, + { 6144000, 0x28 }, + { 9600000, 0x30 }, + { 12288000, 0x33 }, +}; + +static int cs40l26_get_clk_config(struct cs40l26_codec *codec, u32 freq, u8 *clk_cfg) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(cs40l26_pll_sysclk); i++) { + if (cs40l26_pll_sysclk[i].freq == freq) { + *clk_cfg = cs40l26_pll_sysclk[i].cfg; + return 0; + } + } + + dev_err(codec->dev, "Invalid clock frequency: %u Hz\n", freq); + + return -EINVAL; +} + +static int cs40l26_swap_ext_clk(struct cs40l26_codec *codec, u8 clk_src) +{ + u8 clk_cfg, clk_sel; + int ret; + + switch (clk_src) { + case CS40L26_PLL_REFCLK_BCLK: + clk_sel = CS40L26_PLL_CLK_SEL_BCLK; + ret = cs40l26_get_clk_config(codec, codec->rate, &clk_cfg); + break; + case CS40L26_PLL_REFCLK_MCLK: + clk_sel = CS40L26_PLL_CLK_SEL_MCLK; + ret = cs40l26_get_clk_config(codec, 32768, &clk_cfg); + break; + case CS40L26_PLL_REFCLK_FSYNC: + ret = -EPERM; + break; + default: + ret = -EINVAL; + } + + if (ret) { + dev_err(codec->dev, "Failed to get clock configuration\n"); + return ret; + } + + ret = cs40l26_set_pll_loop(codec->core, CS40L26_PLL_OPEN); + if (ret) + return ret; + + ret = regmap_update_bits(codec->regmap, CS40L26_REFCLK_INPUT, + CS40L26_PLL_REFCLK_FREQ_MASK | CS40L26_PLL_REFCLK_SEL_MASK, + (clk_cfg << CS40L26_PLL_REFCLK_FREQ_SHIFT) | clk_sel); + if (ret) + return ret; + + return cs40l26_set_pll_loop(codec->core, CS40L26_PLL_CLOSED); +} + +static int cs40l26_clk_en(struct snd_soc_dapm_widget *w, struct snd_kcontrol *kcontrol, int event) +{ + struct snd_soc_component *component = snd_soc_dapm_to_component(w->dapm); + struct cs40l26_codec *codec = snd_soc_component_get_drvdata(component); + struct cs40l26 *cs40l26 = codec->core; + int ret; + + guard(mutex)(&cs40l26->lock); + + switch (event) { + case SND_SOC_DAPM_POST_PMU: + ret = cs40l26_dsp_write(cs40l26, CS40L26_STOP_PLAYBACK); + if (ret) + return ret; + + ret = regmap_read(codec->regmap, CS40L26_REFCLK_INPUT, &codec->refclk_input); + if (ret) + return ret; + + ret = cs40l26_dsp_write(cs40l26, CS40L26_START_I2S); + if (ret) + return ret; + + ret = cs40l26_swap_ext_clk(codec, CS40L26_PLL_REFCLK_BCLK); + if (ret) + return ret; + break; + case SND_SOC_DAPM_PRE_PMD: + ret = cs40l26_swap_ext_clk(codec, CS40L26_PLL_REFCLK_MCLK); + if (ret) + return ret; + + /* Restore PLL Configuration */ + ret = cs40l26_set_pll_loop(cs40l26, (u32)FIELD_GET(CS40L26_PLL_REFCLK_LOOP_MASK, + codec->refclk_input)); + if (ret) + return ret; + break; + default: + dev_err(codec->dev, "Invalid event: %d\n", event); + return -EINVAL; + } + + return 0; +} + +static int cs40l26_dsp_tx(struct snd_soc_dapm_widget *w, struct snd_kcontrol *kcontrol, int event) +{ + struct snd_soc_component *component = snd_soc_dapm_to_component(w->dapm); + struct cs40l26_codec *codec = snd_soc_component_get_drvdata(component); + struct cs40l26 *cs40l26 = codec->core; + int ret; + + switch (event) { + case SND_SOC_DAPM_POST_PMU: + ret = cs40l26_fw_write(&cs40l26->dsp, "A2HEN", CS40L26_A2H_ALGO_ID, 1); + break; + case SND_SOC_DAPM_PRE_PMD: + ret = cs40l26_fw_write(&cs40l26->dsp, "A2HEN", CS40L26_A2H_ALGO_ID, 0); + break; + default: + dev_err(codec->dev, "Invalid DSPTX event: %d\n", event); + ret = -EINVAL; + } + + return ret; +} + +static int cs40l26_asp_rx(struct snd_soc_dapm_widget *w, struct snd_kcontrol *kcontrol, int event) +{ + struct cs40l26_codec *codec; + struct cs40l26 *cs40l26; + int ret; + + codec = snd_soc_component_get_drvdata(snd_soc_dapm_to_component(w->dapm)); + + cs40l26 = codec->core; + + guard(mutex)(&cs40l26->lock); + + switch (event) { + case SND_SOC_DAPM_POST_PMU: + ret = regmap_update_bits(codec->regmap, CS40L26_DACPCM1_INPUT, + CS40L26_DATA_SRC_MASK, CS40L26_DATA_SRC_DSP1TX1); + if (ret) + return ret; + + ret = regmap_update_bits(codec->regmap, CS40L26_ASPTX1_INPUT, CS40L26_DATA_SRC_MASK, + CS40L26_DATA_SRC_DSP1TX1); + if (ret) + return ret; + + ret = regmap_set_bits(codec->regmap, CS40L26_ASP_ENABLES1, CS40L26_ASP_ENABLE_MASK); + if (ret) + return ret; + break; + case SND_SOC_DAPM_PRE_PMD: + ret = cs40l26_dsp_write(cs40l26, CS40L26_STOP_I2S); + if (ret) + return ret; + + ret = regmap_clear_bits(codec->regmap, CS40L26_ASP_ENABLES1, + CS40L26_ASP_ENABLE_MASK); + if (ret) + return ret; + break; + default: + dev_err(codec->dev, "Invalid ASPRX event: %d\n", event); + return -EINVAL; + } + + return 0; +} + +static int cs40l26_component_set_sysclk(struct snd_soc_component *component, int clk_id, int source, + unsigned int freq, int dir) +{ + struct cs40l26_codec *codec = snd_soc_component_get_drvdata(component); + u8 clk_cfg; + int ret; + + ret = cs40l26_get_clk_config(codec, (u32)(CS40L26_PLL_CLK_FREQ_MASK & freq), &clk_cfg); + if (ret) + return ret; + + if (clk_id) { + dev_err(codec->dev, "Invalid input clock (ID: %d)\n", clk_id); + return -EINVAL; + } + + codec->rate = CS40L26_PLL_CLK_FREQ_MASK & freq; + + return 0; +} + +static int cs40l26_set_dai_fmt(struct snd_soc_dai *codec_dai, unsigned int fmt) +{ + struct cs40l26_codec *codec = snd_soc_component_get_drvdata(codec_dai->component); + + if ((fmt & SND_SOC_DAIFMT_MASTER_MASK) != SND_SOC_DAIFMT_CBC_CFC) { + dev_err(codec->dev, "Device can not be master\n"); + return -EINVAL; + } + + switch (fmt & SND_SOC_DAIFMT_INV_MASK) { + case SND_SOC_DAIFMT_NB_NF: + codec->daifmt = 0; + break; + case SND_SOC_DAIFMT_NB_IF: + codec->daifmt = CS40L26_ASP_FSYNC_INV_MASK; + break; + case SND_SOC_DAIFMT_IB_NF: + codec->daifmt = CS40L26_ASP_BCLK_INV_MASK; + break; + case SND_SOC_DAIFMT_IB_IF: + codec->daifmt = CS40L26_ASP_FSYNC_INV_MASK | CS40L26_ASP_BCLK_INV_MASK; + break; + default: + dev_err(codec->dev, "Invalid clock inversion\n"); + return -EINVAL; + } + + switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { + case SND_SOC_DAIFMT_DSP_A: + codec->daifmt |= FIELD_PREP(CS40L26_ASP_FMT_MASK, CS40L26_ASP_FMT_TDM1_DSPA); + break; + case SND_SOC_DAIFMT_I2S: + codec->daifmt |= FIELD_PREP(CS40L26_ASP_FMT_MASK, CS40L26_ASP_FMT_I2S); + break; + default: + dev_err(codec->dev, "Invalid DAI format: 0x%X\n", fmt & SND_SOC_DAIFMT_FORMAT_MASK); + return -EINVAL; + } + + return 0; +} + +static int cs40l26_pcm_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, struct snd_soc_dai *dai) +{ + struct cs40l26_codec *codec = snd_soc_component_get_drvdata(dai->component); + u32 asp_rx_wl, asp_rx_width; + int ret; + + ret = pm_runtime_resume_and_get(codec->core->dev); + if (ret) + return ret; + + switch (params_rate(params)) { + case 48000: + ret = regmap_clear_bits(codec->regmap, CS40L26_MONITOR_FILT, + CS40L26_VIMON_DUAL_RATE_MASK); + break; + case 96000: + ret = regmap_set_bits(codec->regmap, CS40L26_MONITOR_FILT, + CS40L26_VIMON_DUAL_RATE_MASK); + break; + default: + dev_err(codec->dev, "Unsupported sample rate: %d Hz\n", params_rate(params)); + ret = -EINVAL; + } + + if (ret) + goto pm_exit; + + asp_rx_wl = params_width(params); + + ret = regmap_update_bits(codec->regmap, CS40L26_ASP_DATA_CONTROL5, CS40L26_ASP_RX_WL_MASK, + asp_rx_wl); + if (ret) + goto pm_exit; + + + asp_rx_width = codec->tdm_width ? codec->tdm_width : asp_rx_wl; + + codec->daifmt |= FIELD_PREP(CS40L26_ASP_RX_WIDTH_MASK, asp_rx_width); + + ret = regmap_update_bits(codec->regmap, CS40L26_ASP_CONTROL2, + CS40L26_ASP_FSYNC_INV_MASK | CS40L26_ASP_BCLK_INV_MASK | + CS40L26_ASP_FMT_MASK | CS40L26_ASP_RX_WIDTH_MASK, codec->daifmt); + if (ret) + goto pm_exit; + + ret = regmap_update_bits(codec->regmap, CS40L26_ASP_FRAME_CONTROL5, + CS40L26_ASP_RX1_SLOT_MASK | CS40L26_ASP_RX2_SLOT_MASK, + codec->tdm_slot[0] | + FIELD_PREP(CS40L26_ASP_RX2_SLOT_MASK, codec->tdm_slot[1])); + if (ret) + goto pm_exit; + + dev_dbg(codec->dev, "ASP: %d bits in %d bit slots, slot #s: %d, %d\n", asp_rx_wl, + asp_rx_width, codec->tdm_slot[0], codec->tdm_slot[1]); + +pm_exit: + cs40l26_pm_exit(codec->core->dev); + + return ret; +} + +static int cs40l26_set_tdm_slot(struct snd_soc_dai *dai, unsigned int tx_mask, unsigned int rx_mask, + int slots, int slot_width) +{ + struct cs40l26_codec *codec = snd_soc_component_get_drvdata(dai->component); + + codec->tdm_width = slot_width; + + /* + * Reset slots if TDM is being disabled, and catch the case in which both RX1 and RX2 + * would be set to slot 0 which would cause the hardware to flag an error + */ + if (!slots || rx_mask == 0x1) + rx_mask = 0x3; + + codec->tdm_slot[0] = ffs(rx_mask) - 1; + rx_mask &= ~BIT(codec->tdm_slot[0]); + codec->tdm_slot[1] = ffs(rx_mask) - 1; + + return 0; +} + +static const struct snd_soc_dai_ops cs40l26_dai_ops = { + .set_fmt = cs40l26_set_dai_fmt, + .set_tdm_slot = cs40l26_set_tdm_slot, + .hw_params = cs40l26_pcm_hw_params, +}; + +static struct snd_soc_dai_driver cs40l26_dai[] = { + { + .name = "cs40l26-pcm", + .id = 0, + .playback = { + .stream_name = "ASP Playback", + .channels_min = 1, + .channels_max = 2, + .rates = CS40L26_RATES, + .formats = CS40L26_FORMATS, + }, + .ops = &cs40l26_dai_ops, + .symmetric_rate = 1, + }, +}; + +static const char *const cs40l26_out_mux_texts[] = { "Off", "ASP", "DSP" }; +static SOC_ENUM_SINGLE_VIRT_DECL(cs40l26_out_mux_enum, cs40l26_out_mux_texts); +static const struct snd_kcontrol_new cs40l26_out_mux = + SOC_DAPM_ENUM("Haptics Source", cs40l26_out_mux_enum); + +static const struct snd_soc_dapm_widget cs40l26_dapm_widgets[] = { + SND_SOC_DAPM_SUPPLY_S("ASP PLL", 0, SND_SOC_NOPM, 0, 0, cs40l26_clk_en, + SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_PRE_PMD), + SND_SOC_DAPM_AIF_IN("ASPRX1", NULL, 0, SND_SOC_NOPM, 0, 0), + SND_SOC_DAPM_AIF_IN("ASPRX2", NULL, 0, SND_SOC_NOPM, 0, 0), + + SND_SOC_DAPM_PGA_E("ASP", SND_SOC_NOPM, 0, 0, NULL, 0, cs40l26_asp_rx, + SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_PRE_PMD), + SND_SOC_DAPM_PGA_E("DSP", SND_SOC_NOPM, 0, 0, NULL, 0, cs40l26_dsp_tx, + SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_PRE_PMD), + + SND_SOC_DAPM_MUX("Haptics Source", SND_SOC_NOPM, 0, 0, &cs40l26_out_mux), + SND_SOC_DAPM_OUTPUT("OUT"), +}; + +static const struct snd_soc_dapm_route cs40l26_dapm_routes[] = { + { "ASP Playback", NULL, "ASP PLL" }, + { "ASPRX1", NULL, "ASP Playback" }, + { "ASPRX2", NULL, "ASP Playback" }, + + { "ASP", NULL, "ASPRX1" }, + { "ASP", NULL, "ASPRX2" }, + { "DSP", NULL, "ASP" }, + + { "Haptics Source", "ASP", "ASP" }, + { "Haptics Source", "DSP", "DSP" }, + { "OUT", NULL, "Haptics Source" }, +}; + +static int cs40l26_codec_probe(struct snd_soc_component *component) +{ + struct cs40l26_codec *codec = snd_soc_component_get_drvdata(component); + + /* Default audio SCLK frequency */ + codec->rate = 1536000; + + codec->tdm_slot[0] = 0; + codec->tdm_slot[1] = 1; + + return 0; +} + +static const struct snd_soc_component_driver soc_codec_dev_cs40l26 = { + .probe = cs40l26_codec_probe, + .set_sysclk = cs40l26_component_set_sysclk, + .dapm_widgets = cs40l26_dapm_widgets, + .num_dapm_widgets = ARRAY_SIZE(cs40l26_dapm_widgets), + .dapm_routes = cs40l26_dapm_routes, + .num_dapm_routes = ARRAY_SIZE(cs40l26_dapm_routes), +}; + +static int cs40l26_platform_probe(struct platform_device *pdev) +{ + struct cs40l26 *cs40l26 = dev_get_drvdata(pdev->dev.parent); + struct cs40l26_codec *codec; + + codec = devm_kzalloc(&pdev->dev, sizeof(struct cs40l26_codec), GFP_KERNEL); + if (!codec) + return -ENOMEM; + + codec->core = cs40l26; + codec->regmap = cs40l26->regmap; + codec->dev = &pdev->dev; + + platform_set_drvdata(pdev, codec); + + return snd_soc_register_component(&pdev->dev, &soc_codec_dev_cs40l26, cs40l26_dai, + ARRAY_SIZE(cs40l26_dai)); +} + +static const struct platform_device_id cs40l26_id[] = { + { "cs40l26-codec", }, + {} +}; +MODULE_DEVICE_TABLE(platform, cs40l26_id); + +static struct platform_driver cs40l26_codec_driver = { + .probe = cs40l26_platform_probe, + .id_table = cs40l26_id, + .driver = { + .name = "cs40l26-codec", + }, +}; +module_platform_driver(cs40l26_codec_driver); + +MODULE_DESCRIPTION("ASoC CS40L26 driver"); +MODULE_AUTHOR("Fred Treven ftreven@opensource.cirrus.com"); +MODULE_LICENSE("GPL"); From patchwork Tue Feb 4 23:18:36 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Fred Treven X-Patchwork-Id: 13960207 Received: from mx0b-001ae601.pphosted.com (mx0a-001ae601.pphosted.com [67.231.149.25]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 29EF721C19F; Tue, 4 Feb 2025 23:21:27 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=67.231.149.25 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1738711290; cv=none; b=uRYII6qo8uOdYXJ9DuRhQE61qbKRttdYQWIU0kkfRkba2jZVnaeZ/VcvwmN8wKekHSZaeEhm4kqu8IY5r97NuAc7PeHTwshkSIljrAXxmERocmXq1vJhRt/jqnWzFVdGK3APqAXR7jM5Vx/nmmyQ4orGFjXDsqlqr3yPaJQ1zik= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1738711290; c=relaxed/simple; bh=fuWeLByj80754MOGlmEzAJX/L9c2TYV677lqCSUIZCE=; h=From:To:CC:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=atZX9W/cMtKsOZLGpuC7wiUPjtTcEgPefQ773D5qVMtWrij4sjmRghkox0O+X/ofcMU9BCPCXIooGJapGSVlhPJhFkSI2kXU9gNM2dkz8gxizbNpo7TtYQMYSy1hWGjI4QOXHWAdlmMoZlWfqi+MCzpE/yCVawaW32C/Li6fdAE= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=opensource.cirrus.com; spf=pass smtp.mailfrom=opensource.cirrus.com; dkim=pass (2048-bit key) header.d=cirrus.com header.i=@cirrus.com header.b=R5Wvjvj0; arc=none smtp.client-ip=67.231.149.25 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=opensource.cirrus.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=opensource.cirrus.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=cirrus.com header.i=@cirrus.com header.b="R5Wvjvj0" Received: from pps.filterd (m0077473.ppops.net [127.0.0.1]) by mx0a-001ae601.pphosted.com (8.18.1.2/8.18.1.2) with ESMTP id 514Fmw8O029853; Tue, 4 Feb 2025 17:20:38 -0600 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cirrus.com; h=cc :content-transfer-encoding:content-type:date:from:in-reply-to :message-id:mime-version:references:subject:to; s= PODMain02222019; bh=MF3LR1s1Rj7Ha12pV01Rjj4AA/V+w0HEuDHAs9QG5WU=; b= R5Wvjvj0Dsy0DDp/BN6Zk14Dyn9fcvIJQJp4od9A3zHV8SrmXvWTaeVya5ehmJoL UpJtczubkaD+7+3FvdtAT7KVtnvWm4emQNP+cij6kYNvRAJJY5LB3A6LAMO17CMm MyWui2nT7bQHNOLrpsI9w9sFgybqBTjLNMcnhut7DEIbgd4oOJQlLUwh8NJuxn1T 8OGhYBsuoSL7Pre3MKWa9Zg9D0eyR8RjUM15yWaYKxNEBzkxsOFewrb/TPqMICDW 43HjBnpbVizzkZ+UnzU9mTLaEcZF8odZYtJ+R8vV2VujuTJTNGv2AT2Qi8kq8zfc FZ570fke7jpAfsQq8m+ljg== Received: from ediex02.ad.cirrus.com ([84.19.233.68]) by mx0a-001ae601.pphosted.com (PPS) with ESMTPS id 44hhw53pkt-1 (version=TLSv1.2 cipher=ECDHE-RSA-AES256-GCM-SHA384 bits=256 verify=NOT); Tue, 04 Feb 2025 17:20:37 -0600 (CST) Received: from ediex01.ad.cirrus.com (198.61.84.80) by ediex02.ad.cirrus.com (198.61.84.81) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.2.1544.14; Tue, 4 Feb 2025 23:20:25 +0000 Received: from ediswmail9.ad.cirrus.com (198.61.86.93) by anon-ediex01.ad.cirrus.com (198.61.84.80) with Microsoft SMTP Server id 15.2.1544.14 via Frontend Transport; Tue, 4 Feb 2025 23:20:20 +0000 Received: from ftrev.crystal.cirrus.com (ftrev.ad.cirrus.com [141.131.145.81]) by ediswmail9.ad.cirrus.com (Postfix) with ESMTP id 85475820270; Tue, 4 Feb 2025 23:20:16 +0000 (UTC) From: Fred Treven To: Lee Jones , Rob Herring , "Krzysztof Kozlowski" , Conor Dooley , "Simon Trimmer" , Charles Keepax , Richard Fitzgerald , Dmitry Torokhov , James Ogletree , Ben Bright , Liam Girdwood , Mark Brown , Jaroslav Kysela , Takashi Iwai , David Rhodes , Jeff LaBundy , Heiko Stuebner , Karel Balej , Igor Prusov , Jack Yu , Weidong Wang , Binbin Zhou , Prasad Kumpatla , "Paul Handrigan" , Masahiro Yamada , Nuno Sa , Fred Treven CC: , , , , , Subject: [PATCH RESEND 7/7] Input: cs40l26 - Add support for CS40L26 haptic driver Date: Tue, 4 Feb 2025 17:18:36 -0600 Message-ID: <20250204231835.2000457-8-ftreven@opensource.cirrus.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20250204231835.2000457-1-ftreven@opensource.cirrus.com> References: <20250204231835.2000457-1-ftreven@opensource.cirrus.com> Precedence: bulk X-Mailing-List: linux-input@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Proofpoint-ORIG-GUID: c-eADMAq4RV4r0b5gCbXUlG5TL3rnwsb X-Authority-Analysis: v=2.4 cv=W/3CVQWk c=1 sm=1 tr=0 ts=67a2a0c5 cx=c_pps a=uGhh+3tQvKmCLpEUO+DX4w==:117 a=uGhh+3tQvKmCLpEUO+DX4w==:17 a=T2h4t0Lz3GQA:10 a=w1d2syhTAAAA:8 a=u19Cq9sczE363Rxt_jsA:9 a=YXXWInSmI4Sqt1AkVdoW:22 X-Proofpoint-GUID: c-eADMAq4RV4r0b5gCbXUlG5TL3rnwsb X-Proofpoint-Spam-Reason: safe Introduce support for Cirrus Logic Device CS40L26: a boosted haptics driver with integrated DSP and waveform memory with advanced closed loop algorithms and LRA protection. The input driver provides the interface for control of haptic effects through the device. Signed-off-by: Fred Treven --- drivers/input/misc/Kconfig | 10 + drivers/input/misc/Makefile | 1 + drivers/input/misc/cs40l26-vibra.c | 669 +++++++++++++++++++++++++++++ 3 files changed, 680 insertions(+) create mode 100644 drivers/input/misc/cs40l26-vibra.c diff --git a/drivers/input/misc/Kconfig b/drivers/input/misc/Kconfig index 13d135257e06..2c9496c873e7 100644 --- a/drivers/input/misc/Kconfig +++ b/drivers/input/misc/Kconfig @@ -147,6 +147,16 @@ config INPUT_BMA150 To compile this driver as a module, choose M here: the module will be called bma150. +config INPUT_CS40L26_VIBRA + tristate "CS40L26 Haptic Driver support" + depends on MFD_CS40L26_CORE + help + Say Y here to enable support for Cirrus Logic's CS40L26 + haptic driver. + + To compile this driver as a module, choose M here: the + module will be called cs40l26-vibra. + config INPUT_CS40L50_VIBRA tristate "CS40L50 Haptic Driver support" depends on MFD_CS40L50_CORE diff --git a/drivers/input/misc/Makefile b/drivers/input/misc/Makefile index 6d91804d0a6f..b6274a937a94 100644 --- a/drivers/input/misc/Makefile +++ b/drivers/input/misc/Makefile @@ -29,6 +29,7 @@ obj-$(CONFIG_INPUT_CMA3000) += cma3000_d0x.o obj-$(CONFIG_INPUT_CMA3000_I2C) += cma3000_d0x_i2c.o obj-$(CONFIG_INPUT_COBALT_BTNS) += cobalt_btns.o obj-$(CONFIG_INPUT_CPCAP_PWRBUTTON) += cpcap-pwrbutton.o +obj-$(CONFIG_INPUT_CS40L26_VIBRA) += cs40l26-vibra.o obj-$(CONFIG_INPUT_CS40L50_VIBRA) += cs40l50-vibra.o obj-$(CONFIG_INPUT_DA7280_HAPTICS) += da7280.o obj-$(CONFIG_INPUT_DA9052_ONKEY) += da9052_onkey.o diff --git a/drivers/input/misc/cs40l26-vibra.c b/drivers/input/misc/cs40l26-vibra.c new file mode 100644 index 000000000000..d083be714a3a --- /dev/null +++ b/drivers/input/misc/cs40l26-vibra.c @@ -0,0 +1,669 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * CS40L26 Advanced Haptic Driver with waveform memory, + * integrated DSP, and closed-loop algorithms + * + * Copyright 2025 Cirrus Logic, Inc. + * + * Author: Fred Treven + */ + +#include +#include +#include + +#define CS40L26_EFFECTS_MAX 1 + +#define CS40L26_NUM_PCT_MAP_VALUES 101 + +#define CS40L26_STOP_PLAYBACK 0x05000000 + +#define CS40L26_MAX_INDEX_MASK GENMASK(15, 0) + +#define CS40L26_RAM_INDEX_START 0x01000000 +#define CS40L26_RAM_INDEX_END 0x0100007F + +#define CS40L26_ROM_INDEX_START 0x01800000 +#define CS40L26_ROM_INDEX_END 0x01800026 +#define CS40L26_NUM_ROM_WAVES (CS40L26_ROM_INDEX_END - CS40L26_ROM_INDEX_START + 1) + +#define CS40L26_BUZZGEN_INDEX_START 0x01800080 +#define CS40L26_BUZZGEN_INDEX_END 0x01800085 + +#define CS40L26_BUZZGEN_PER_MS_MAX 10 +#define CS40L26_BUZZGEN_PER_MS_MIN 4 + +#define CS40L26_BUZZGEN_LEVEL_MIN 0x00 +#define CS40L26_BUZZGEN_LEVEL_MAX 0xFF + +#define CS40L26_BUZZGEN_NUM_CONFIGS (CS40L26_BUZZGEN_INDEX_END - CS40L26_BUZZGEN_INDEX_START) + +enum cs40l26_bank { + CS40L26_BANK_RAM, + CS40L26_BANK_ROM, + CS40L26_BANK_BUZ, +}; + +struct cs40l26_effect { + enum cs40l26_bank bank; + u32 index; + int id; + struct list_head list; +}; + +struct cs40l26_vibra { + struct cs40l26 *cs40l26; + struct input_dev *input; + struct workqueue_struct *vib_wq; + struct list_head effect_head; +}; + +struct cs40l26_work { + struct ff_effect *ff_effect; + struct cs40l26_vibra *vib; + struct work_struct work; + s16 *custom_data; + int custom_len; + u16 gain_pct; + int count; + int error; +}; + +struct cs40l26_buzzgen_config { + const char *duration_name; + const char *freq_name; + const char *level_name; + int effect_id; +}; + +static struct cs40l26_buzzgen_config cs40l26_buzzgen_configs[] = { + { + .duration_name = "BUZZ_EFFECTS2_BUZZ_DURATION", + .freq_name = "BUZZ_EFFECTS2_BUZZ_FREQ", + .level_name = "BUZZ_EFFECTS2_BUZZ_LEVEL", + .effect_id = -1 + }, + { + .duration_name = "BUZZ_EFFECTS3_BUZZ_DURATION", + .freq_name = "BUZZ_EFFECTS3_BUZZ_FREQ", + .level_name = "BUZZ_EFFECTS3_BUZZ_LEVEL", + .effect_id = -1 + }, + { + .duration_name = "BUZZ_EFFECTS4_BUZZ_DURATION", + .freq_name = "BUZZ_EFFECTS4_BUZZ_FREQ", + .level_name = "BUZZ_EFFECTS4_BUZZ_LEVEL", + .effect_id = -1 + }, + { + .duration_name = "BUZZ_EFFECTS5_BUZZ_DURATION", + .freq_name = "BUZZ_EFFECTS5_BUZZ_FREQ", + .level_name = "BUZZ_EFFECTS5_BUZZ_LEVEL", + .effect_id = -1 + }, + { + .duration_name = "BUZZ_EFFECTS6_BUZZ_DURATION", + .freq_name = "BUZZ_EFFECTS6_BUZZ_FREQ", + .level_name = "BUZZ_EFFECTS6_BUZZ_LEVEL", + .effect_id = -1 + }, +}; + +static int cs40l26_buzzgen_find_slot(int id) +{ + int effect_id, lowest_available_slot = -1, slot; + + for (slot = CS40L26_BUZZGEN_NUM_CONFIGS - 1; slot >= 0; slot--) { + effect_id = cs40l26_buzzgen_configs[slot].effect_id; + + if (effect_id == id) + return slot; + else if (effect_id == -1) + lowest_available_slot = slot; + } + + return lowest_available_slot; +} + +static int cs40l26_sine_upload(struct cs40l26_vibra *vib, struct cs40l26_work *work_data, + struct cs40l26_effect *effect) +{ + struct cs_dsp *dsp = &vib->cs40l26->dsp; + unsigned int duration, freq, level; + int error, slot; + + slot = cs40l26_buzzgen_find_slot(work_data->ff_effect->id); + if (slot == -1) { + dev_err(vib->cs40l26->dev, "No free BUZZGEN slot available\n"); + return -ENOSPC; + } + + cs40l26_buzzgen_configs[slot].effect_id = work_data->ff_effect->id; + + /* Firmware expects duration in ms divided by 4 */ + duration = (unsigned int)DIV_ROUND_UP(work_data->ff_effect->replay.length, 4); + + freq = (unsigned int)(1000 / clamp_val(work_data->ff_effect->u.periodic.period, + CS40L26_BUZZGEN_PER_MS_MIN, + CS40L26_BUZZGEN_PER_MS_MAX)); + + level = (unsigned int)clamp_val(work_data->ff_effect->u.periodic.magnitude, + CS40L26_BUZZGEN_LEVEL_MIN, CS40L26_BUZZGEN_LEVEL_MAX); + + guard(mutex)(&dsp->pwr_lock); + + error = cs40l26_fw_write(dsp, cs40l26_buzzgen_configs[slot].duration_name, + CS40L26_BUZZGEN_ALGO_ID, duration); + if (error) + return error; + + error = cs40l26_fw_write(dsp, cs40l26_buzzgen_configs[slot].freq_name, + CS40L26_BUZZGEN_ALGO_ID, freq); + if (error) + return error; + + error = cs40l26_fw_write(dsp, cs40l26_buzzgen_configs[slot].level_name, + CS40L26_BUZZGEN_ALGO_ID, level); + if (error) + return error; + + effect->id = work_data->ff_effect->id; + effect->bank = CS40L26_BANK_BUZ; + + /* BUZZGEN slot 1 is reserved for OTP buzz so offset of 1 required */ + effect->index = CS40L26_BUZZGEN_INDEX_START + slot + 1; + + return 0; +} + +static int cs40l26_num_ram_waves(struct cs40l26_vibra *vib) +{ + u32 nwaves; + int error; + + guard(mutex)(&vib->cs40l26->dsp.pwr_lock); + + error = cs40l26_fw_read(&vib->cs40l26->dsp, "NUM_OF_WAVES", + vib->cs40l26->variant->info->vibegen_algo_id, &nwaves); + + return error ? error : (int)nwaves; +} + +static int cs40l26_trigger_index_get(struct cs40l26_vibra *vib, struct cs40l26_work *work_data, + enum cs40l26_bank bank, u32 *trigger_index) +{ + u16 index = (u16)(work_data->custom_data[1] & CS40L26_MAX_INDEX_MASK); + struct device *dev = vib->cs40l26->dev; + int error = 0, nwaves; + u32 index_start; + + switch (bank) { + case CS40L26_BANK_RAM: + nwaves = cs40l26_num_ram_waves(vib); + if (nwaves < 0) { + error = nwaves; + } else if (nwaves == 0) { + dev_err(dev, "No waveforms in RAM bank\n"); + error = -ENODATA; + } + + index_start = CS40L26_RAM_INDEX_START; + break; + case CS40L26_BANK_ROM: + nwaves = CS40L26_NUM_ROM_WAVES; + index_start = CS40L26_ROM_INDEX_START; + break; + default: + dev_err(dev, "Invalid bank %u\n", bank); + error = -EINVAL; + } + + if (error) + return error; + + if (index > nwaves - 1) { + dev_err(dev, "Index %u invalid for bank %u (%d waveforms)\n", index, bank, nwaves); + return -EINVAL; + } + + *trigger_index = index + index_start; + + return 0; +} + +static int cs40l26_custom_upload(struct cs40l26_vibra *vib, struct cs40l26_work *work_data, + struct cs40l26_effect *effect) +{ + size_t data_len = work_data->ff_effect->u.periodic.custom_len; + enum cs40l26_bank bank; + int error; + + if (data_len != 2) { + dev_err(vib->cs40l26->dev, "Invalid custom data length %zd\n", data_len); + return -EINVAL; + } + + bank = (enum cs40l26_bank)work_data->custom_data[0]; + + error = cs40l26_trigger_index_get(vib, work_data, bank, &effect->index); + if (error) + return error; + + effect->id = work_data->ff_effect->id; + effect->bank = bank; + + return 0; +} + +static struct cs40l26_effect *cs40l26_find_effect(struct cs40l26_vibra *vib, int id) +{ + struct cs40l26_effect *effect; + + if (list_empty(&vib->effect_head)) + return NULL; + + list_for_each_entry(effect, &vib->effect_head, list) { + if (effect->id == id) + return effect; + } + + return NULL; +} + +static void cs40l26_upload_worker(struct work_struct *work) +{ + struct cs40l26_work *work_data = container_of(work, struct cs40l26_work, work); + struct cs40l26_vibra *vib = work_data->vib; + struct device *dev = vib->cs40l26->dev; + struct cs40l26_effect *effect; + bool new_effect = false; + int error; + + error = pm_runtime_resume_and_get(dev); + if (error) { + work_data->error = error; + return; + } + + effect = cs40l26_find_effect(vib, work_data->ff_effect->id); + if (!effect) { + effect = devm_kzalloc(dev, sizeof(struct cs40l26_effect), GFP_KERNEL); + if (!effect) { + cs40l26_pm_exit(dev); + + work_data->error = -ENOMEM; + return; + } + + new_effect = true; + } + + if (work_data->ff_effect->u.periodic.waveform == FF_CUSTOM) { + error = cs40l26_custom_upload(vib, work_data, effect); + } else if (work_data->ff_effect->u.periodic.waveform == FF_SINE) { + error = cs40l26_sine_upload(vib, work_data, effect); + } else { + dev_err(dev, "Type 0x%X unsupported\n", work_data->ff_effect->u.periodic.waveform); + error = -EINVAL; + } + + if (error) { + if (new_effect) + devm_kfree(dev, effect); + + cs40l26_pm_exit(dev); + + work_data->error = error; + return; + } + + if (new_effect) + list_add(&effect->list, &vib->effect_head); + + cs40l26_pm_exit(dev); + + work_data->error = 0; +} + +static int cs40l26_upload(struct input_dev *dev, struct ff_effect *effect, struct ff_effect *old) +{ + struct cs40l26_vibra *vib = input_get_drvdata(dev); + bool custom = false; + struct cs40l26_work *work_data; + int error; + + work_data = kzalloc(sizeof(struct cs40l26_work), GFP_KERNEL); + if (!work_data) + return -ENOMEM; + + if (effect->u.periodic.waveform == FF_CUSTOM) { + work_data->custom_data = memdup_array_user(effect->u.periodic.custom_data, + effect->u.periodic.custom_len, + sizeof(s16)); + if (IS_ERR(work_data->custom_data)) { + error = PTR_ERR(work_data->custom_data); + goto out_free; + } + + custom = true; + work_data->custom_len = effect->u.periodic.custom_len; + } + + work_data->vib = vib; + work_data->ff_effect = effect; + + INIT_WORK(&work_data->work, cs40l26_upload_worker); + + queue_work(vib->vib_wq, &work_data->work); + flush_work(&work_data->work); + + error = work_data->error; + +out_free: + if (custom) + kfree(work_data->custom_data); + + kfree(work_data); + + return error; +} + +static void cs40l26_stop_playback_worker(struct work_struct *work) +{ + struct cs40l26_work *work_data = container_of(work, struct cs40l26_work, work); + struct cs40l26_vibra *vib = work_data->vib; + + if (pm_runtime_resume_and_get(vib->cs40l26->dev)) + goto out_free; + + if (cs40l26_dsp_write(vib->cs40l26, CS40L26_STOP_PLAYBACK)) + dev_err(vib->cs40l26->dev, "Failed to stop haptic playback\n"); + + cs40l26_pm_exit(vib->cs40l26->dev); +out_free: + kfree(work_data); +} + +static void cs40l26_start_playback_worker(struct work_struct *work) +{ + struct cs40l26_work *work_data = container_of(work, struct cs40l26_work, work); + struct cs40l26 *cs40l26 = work_data->vib->cs40l26; + struct cs40l26_effect *effect; + u16 duration; + int id; + + id = work_data->ff_effect->id; + + duration = work_data->ff_effect->replay.length; + + if (pm_runtime_resume_and_get(cs40l26->dev)) + goto out_free; + + guard(mutex)(&cs40l26->dsp.pwr_lock); + + if (cs40l26_fw_write(&cs40l26->dsp, "TIMEOUT_MS", cs40l26->variant->info->vibegen_algo_id, + duration)) + goto out_pm; + + effect = cs40l26_find_effect(work_data->vib, id); + if (effect) { + while (--work_data->count >= 0) { + if (cs40l26_dsp_write(cs40l26, effect->index)) + goto out_pm; + + usleep_range(duration, duration + 100); + } + } else { + dev_err(cs40l26->dev, "No effect found with ID %d\n", id); + } + +out_pm: + cs40l26_pm_exit(cs40l26->dev); + +out_free: + kfree(work_data); +} + +static int cs40l26_playback(struct input_dev *dev, int effect_id, int val) +{ + struct cs40l26_vibra *vib = input_get_drvdata(dev); + struct cs40l26_work *work_data; + + work_data = kzalloc(sizeof(struct cs40l26_work), GFP_ATOMIC); + if (!work_data) + return -ENOMEM; + + work_data->vib = vib; + + if (val > 0) { + work_data->ff_effect = &dev->ff->effects[effect_id]; + work_data->count = val; + INIT_WORK(&work_data->work, cs40l26_start_playback_worker); + } else { + INIT_WORK(&work_data->work, cs40l26_stop_playback_worker); + } + + queue_work(vib->vib_wq, &work_data->work); + + return 0; +} + +static int cs40l26_sine_erase(struct cs40l26_vibra *vib, int id) +{ + int slot = cs40l26_buzzgen_find_slot(id); + + if (slot == -1) { + dev_err(vib->cs40l26->dev, "No BUZZGEN ID matching %d\n", id); + return -EINVAL; + } + + cs40l26_buzzgen_configs[slot].effect_id = -1; + + return 0; +} + +static void cs40l26_erase_worker(struct work_struct *work) +{ + struct cs40l26_work *work_data = container_of(work, struct cs40l26_work, work); + struct cs40l26_vibra *vib = work_data->vib; + struct device *dev = vib->cs40l26->dev; + int id = work_data->ff_effect->id; + struct cs40l26_effect *effect; + int error; + + error = pm_runtime_resume_and_get(dev); + if (error) { + work_data->error = error; + return; + } + + effect = cs40l26_find_effect(vib, id); + if (!effect) { + dev_err(dev, "Cannot erase effect with ID %d, no such effect\n", id); + error = -EINVAL; + goto out_pm; + } + + if (effect->bank == CS40L26_BANK_BUZ) { + error = cs40l26_sine_erase(vib, id); + if (error) + goto out_pm; + } + + list_del(&effect->list); + devm_kfree(dev, effect); + +out_pm: + cs40l26_pm_exit(dev); + + work_data->error = error; +} + +static int cs40l26_erase(struct input_dev *dev, int effect_id) +{ + struct cs40l26_vibra *vib = input_get_drvdata(dev); + struct cs40l26_work *work_data; + int error; + + work_data = kzalloc(sizeof(struct cs40l26_work), GFP_KERNEL); + if (!work_data) + return -ENOMEM; + + work_data->vib = vib; + work_data->error = 0; + work_data->ff_effect = &dev->ff->effects[effect_id]; + + INIT_WORK(&work_data->work, cs40l26_erase_worker); + + queue_work(vib->vib_wq, &work_data->work); + flush_work(&work_data->work); + + error = work_data->error; + + kfree(work_data); + + return error; +} + +/* LUT for converting gain percentage to attenuation in dB */ +static const u32 cs40l26_atten_lut_q21_2[CS40L26_NUM_PCT_MAP_VALUES] = { + /* MUTE */ 400, 160, 136, 122, 112, 104, 98, 92, 88, 84, 80, 77, 74, + 71, 68, 66, 64, 62, 60, 58, 56, 54, 53, 51, 50, 48, 47, 45, 44, 43, + 42, 41, 40, 39, 37, 36, 35, 35, 34, 33, 32, 31, 30, 29, 29, 28, 27, + 26, 26, 25, 24, 23, 23, 22, 21, 21, 20, 20, 19, 18, 18, 17, 17, 16, + 16, 15, 14, 14, 13, 13, 12, 12, 11, 11, 10, 10, 10, 9, 9, 8, 8, 7, + 7, 6, 6, 6, 5, 5, 4, 4, 4, 3, 3, 3, 2, 2, 1, 1, 1, 0, 0, /* 100% */ +}; + +static void cs40l26_set_gain_worker(struct work_struct *work) +{ + struct cs40l26_work *work_data = container_of(work, struct cs40l26_work, work); + struct cs40l26_vibra *vib = work_data->vib; + struct cs40l26 *cs40l26 = vib->cs40l26; + int error; + + error = pm_runtime_resume_and_get(vib->cs40l26->dev); + if (error) { + dev_err(vib->cs40l26->dev, "%s: Failed to exit hibernate\n", __func__); + goto out_free; + } + + guard(mutex)(&vib->cs40l26->dsp.pwr_lock); + + error = cs40l26_fw_write(&vib->cs40l26->dsp, "SOURCE_ATTENUATION", + cs40l26->variant->info->ram_ext_algo_id, + cs40l26_atten_lut_q21_2[work_data->gain_pct]); + if (error) + dev_err(vib->cs40l26->dev, "Failed to set attenuation\n"); + + cs40l26_pm_exit(vib->cs40l26->dev); + +out_free: + kfree(work_data); +} + +static void cs40l26_set_gain(struct input_dev *dev, u16 gain) +{ + struct cs40l26_vibra *vib = input_get_drvdata(dev); + struct cs40l26_work *work_data; + + if (gain >= CS40L26_NUM_PCT_MAP_VALUES) { + dev_err(vib->cs40l26->dev, "Gain value %u%% out of bounds\n", gain); + return; + } + + work_data = kzalloc(sizeof(struct cs40l26_work), GFP_ATOMIC); + if (!work_data) + return; + + work_data->gain_pct = gain; + work_data->vib = vib; + + INIT_WORK(&work_data->work, cs40l26_set_gain_worker); + + queue_work(vib->vib_wq, &work_data->work); +} + +static void cs40l26_remove_wq(void *data) +{ + flush_workqueue(data); + destroy_workqueue((struct workqueue_struct *)data); +} + +static int cs40l26_vibra_probe(struct platform_device *pdev) +{ + struct cs40l26 *cs40l26 = dev_get_drvdata(pdev->dev.parent); + struct cs40l26_vibra *vib; + int error; + + vib = devm_kzalloc(cs40l26->dev, sizeof(struct cs40l26_vibra), GFP_KERNEL); + if (!vib) + return -ENOMEM; + + vib->cs40l26 = cs40l26; + + vib->input = devm_input_allocate_device(vib->cs40l26->dev); + if (!vib->input) + return -ENOMEM; + + vib->input->id.product = cs40l26->devid; + vib->input->id.version = cs40l26->revid; + vib->input->name = "cs40l26_vibra"; + + input_set_drvdata(vib->input, vib); + input_set_capability(vib->input, EV_FF, FF_PERIODIC); + input_set_capability(vib->input, EV_FF, FF_CUSTOM); + input_set_capability(vib->input, EV_FF, FF_SINE); + input_set_capability(vib->input, EV_FF, FF_GAIN); + + error = input_ff_create(vib->input, 1); + if (error) { + dev_err(vib->cs40l26->dev, "Failed to create input device\n"); + return error; + } + + clear_bit(FF_RUMBLE, vib->input->ffbit); + + vib->input->ff->upload = cs40l26_upload; + vib->input->ff->playback = cs40l26_playback; + vib->input->ff->set_gain = cs40l26_set_gain; + vib->input->ff->erase = cs40l26_erase; + + INIT_LIST_HEAD(&vib->effect_head); + + vib->vib_wq = alloc_ordered_workqueue("vib_wq", WQ_HIGHPRI); + if (!vib->vib_wq) + return -ENOMEM; + + error = devm_add_action_or_reset(vib->cs40l26->dev, cs40l26_remove_wq, vib->vib_wq); + if (error) + return error; + + error = input_register_device(vib->input); + if (error) + return error; + + dev_info(vib->cs40l26->dev, "Loaded cs40l26-vibra with %d RAM waveforms\n", + cs40l26_num_ram_waves(vib)); + + return 0; +} + +static const struct platform_device_id cs40l26_vibra_id_match[] = { + { "cs40l26-vibra", }, + {} +}; +MODULE_DEVICE_TABLE(platform, cs40l26_vibra_id_match); + +static struct platform_driver cs40l26_vibra_driver = { + .probe = cs40l26_vibra_probe, + .id_table = cs40l26_vibra_id_match, + .driver = { + .name = "cs40l26-vibra", + }, +}; +module_platform_driver(cs40l26_vibra_driver); + +MODULE_DESCRIPTION("CS40L26 Boosted Class D Amplifier for Haptics"); +MODULE_AUTHOR("Fred Treven, Cirrus Logic Inc. "); +MODULE_LICENSE("GPL");