12874c5fdSThomas Gleixner // SPDX-License-Identifier: GPL-2.0-or-later
2d1496c39SNicolas Pitre /*
3d1496c39SNicolas Pitre * linux/drivers/mmc/core/sdio_irq.c
4d1496c39SNicolas Pitre *
5d1496c39SNicolas Pitre * Author: Nicolas Pitre
6d1496c39SNicolas Pitre * Created: June 18, 2007
7d1496c39SNicolas Pitre * Copyright: MontaVista Software Inc.
8d1496c39SNicolas Pitre *
9e633b7bcSPierre Ossman * Copyright 2008 Pierre Ossman
10d1496c39SNicolas Pitre */
11d1496c39SNicolas Pitre
12d1496c39SNicolas Pitre #include <linux/kernel.h>
13d1496c39SNicolas Pitre #include <linux/sched.h>
14ae7e81c0SIngo Molnar #include <uapi/linux/sched/types.h>
15d1496c39SNicolas Pitre #include <linux/kthread.h>
163ef77af1SPaul Gortmaker #include <linux/export.h>
17d1496c39SNicolas Pitre #include <linux/wait.h>
18d1496c39SNicolas Pitre #include <linux/delay.h>
19d1496c39SNicolas Pitre
20d1496c39SNicolas Pitre #include <linux/mmc/core.h>
21d1496c39SNicolas Pitre #include <linux/mmc/host.h>
22d1496c39SNicolas Pitre #include <linux/mmc/card.h>
23d1496c39SNicolas Pitre #include <linux/mmc/sdio.h>
24d1496c39SNicolas Pitre #include <linux/mmc/sdio_func.h>
25d1496c39SNicolas Pitre
26d1496c39SNicolas Pitre #include "sdio_ops.h"
2755244c56SUlf Hansson #include "core.h"
284facdde1SUlf Hansson #include "card.h"
29d1496c39SNicolas Pitre
sdio_get_pending_irqs(struct mmc_host * host,u8 * pending)30a29b5fccSMatthias Kaehlcke static int sdio_get_pending_irqs(struct mmc_host *host, u8 *pending)
31a29b5fccSMatthias Kaehlcke {
32a29b5fccSMatthias Kaehlcke struct mmc_card *card = host->card;
33a29b5fccSMatthias Kaehlcke int ret;
34a29b5fccSMatthias Kaehlcke
35a29b5fccSMatthias Kaehlcke WARN_ON(!host->claimed);
36a29b5fccSMatthias Kaehlcke
37a29b5fccSMatthias Kaehlcke ret = mmc_io_rw_direct(card, 0, 0, SDIO_CCCR_INTx, 0, pending);
38a29b5fccSMatthias Kaehlcke if (ret) {
39a29b5fccSMatthias Kaehlcke pr_debug("%s: error %d reading SDIO_CCCR_INTx\n",
40a29b5fccSMatthias Kaehlcke mmc_card_id(card), ret);
41a29b5fccSMatthias Kaehlcke return ret;
42a29b5fccSMatthias Kaehlcke }
43a29b5fccSMatthias Kaehlcke
44a29b5fccSMatthias Kaehlcke if (*pending && mmc_card_broken_irq_polling(card) &&
45a29b5fccSMatthias Kaehlcke !(host->caps & MMC_CAP_SDIO_IRQ)) {
46a29b5fccSMatthias Kaehlcke unsigned char dummy;
47a29b5fccSMatthias Kaehlcke
48a29b5fccSMatthias Kaehlcke /* A fake interrupt could be created when we poll SDIO_CCCR_INTx
49a29b5fccSMatthias Kaehlcke * register with a Marvell SD8797 card. A dummy CMD52 read to
50a29b5fccSMatthias Kaehlcke * function 0 register 0xff can avoid this.
51a29b5fccSMatthias Kaehlcke */
52a29b5fccSMatthias Kaehlcke mmc_io_rw_direct(card, 0, 0, 0xff, 0, &dummy);
53a29b5fccSMatthias Kaehlcke }
54a29b5fccSMatthias Kaehlcke
55a29b5fccSMatthias Kaehlcke return 0;
56a29b5fccSMatthias Kaehlcke }
57a29b5fccSMatthias Kaehlcke
process_sdio_pending_irqs(struct mmc_host * host)58bbbc4c4dSNicolas Pitre static int process_sdio_pending_irqs(struct mmc_host *host)
59d1496c39SNicolas Pitre {
60bbbc4c4dSNicolas Pitre struct mmc_card *card = host->card;
616f4285d1SPierre Ossman int i, ret, count;
6236d57efbSUlf Hansson bool sdio_irq_pending = host->sdio_irq_pending;
63d1496c39SNicolas Pitre unsigned char pending;
6406e8935fSStefan Nilsson XK struct sdio_func *func;
6506e8935fSStefan Nilsson XK
6683293386SUlf Hansson /* Don't process SDIO IRQs if the card is suspended. */
6783293386SUlf Hansson if (mmc_card_suspended(card))
6883293386SUlf Hansson return 0;
6983293386SUlf Hansson
7036d57efbSUlf Hansson /* Clear the flag to indicate that we have processed the IRQ. */
7136d57efbSUlf Hansson host->sdio_irq_pending = false;
7236d57efbSUlf Hansson
7306e8935fSStefan Nilsson XK /*
7406e8935fSStefan Nilsson XK * Optimization, if there is only 1 function interrupt registered
75bbbc4c4dSNicolas Pitre * and we know an IRQ was signaled then call irq handler directly.
76bbbc4c4dSNicolas Pitre * Otherwise do the full probe.
7706e8935fSStefan Nilsson XK */
7806e8935fSStefan Nilsson XK func = card->sdio_single_irq;
7936d57efbSUlf Hansson if (func && sdio_irq_pending) {
8006e8935fSStefan Nilsson XK func->irq_handler(func);
8106e8935fSStefan Nilsson XK return 1;
8206e8935fSStefan Nilsson XK }
83d1496c39SNicolas Pitre
84a29b5fccSMatthias Kaehlcke ret = sdio_get_pending_irqs(host, &pending);
85a29b5fccSMatthias Kaehlcke if (ret)
86d1496c39SNicolas Pitre return ret;
87e5624054SBing Zhao
886f4285d1SPierre Ossman count = 0;
89d1496c39SNicolas Pitre for (i = 1; i <= 7; i++) {
90d1496c39SNicolas Pitre if (pending & (1 << i)) {
9106e8935fSStefan Nilsson XK func = card->sdio_func[i - 1];
92d1496c39SNicolas Pitre if (!func) {
936606110dSJoe Perches pr_warn("%s: pending IRQ for non-existent function\n",
943e01e4bcSNicolas Pitre mmc_card_id(card));
95599473cfSNicolas Pitre ret = -EINVAL;
96d1496c39SNicolas Pitre } else if (func->irq_handler) {
97d1496c39SNicolas Pitre func->irq_handler(func);
986f4285d1SPierre Ossman count++;
99599473cfSNicolas Pitre } else {
1006606110dSJoe Perches pr_warn("%s: pending IRQ with no handler\n",
101d1496c39SNicolas Pitre sdio_func_id(func));
102599473cfSNicolas Pitre ret = -EINVAL;
103599473cfSNicolas Pitre }
104d1496c39SNicolas Pitre }
105d1496c39SNicolas Pitre }
106d1496c39SNicolas Pitre
107599473cfSNicolas Pitre if (count)
1086f4285d1SPierre Ossman return count;
109599473cfSNicolas Pitre
110599473cfSNicolas Pitre return ret;
111d1496c39SNicolas Pitre }
112d1496c39SNicolas Pitre
sdio_run_irqs(struct mmc_host * host)113cf4b20ecSUlf Hansson static void sdio_run_irqs(struct mmc_host *host)
114bf3b5ec6SRussell King {
115bf3b5ec6SRussell King mmc_claim_host(host);
116e3a84267SUlf Hansson if (host->sdio_irqs) {
117bf3b5ec6SRussell King process_sdio_pending_irqs(host);
11851133850SUlf Hansson if (!host->sdio_irq_pending)
11968269660SUlf Hansson host->ops->ack_sdio_irq(host);
120e3a84267SUlf Hansson }
121bf3b5ec6SRussell King mmc_release_host(host);
122bf3b5ec6SRussell King }
123bf3b5ec6SRussell King
sdio_irq_work(struct work_struct * work)12468269660SUlf Hansson void sdio_irq_work(struct work_struct *work)
12568269660SUlf Hansson {
12668269660SUlf Hansson struct mmc_host *host =
127*1dd611a9SHeiner Kallweit container_of(work, struct mmc_host, sdio_irq_work);
12868269660SUlf Hansson
12968269660SUlf Hansson sdio_run_irqs(host);
13068269660SUlf Hansson }
13168269660SUlf Hansson
sdio_signal_irq(struct mmc_host * host)13268269660SUlf Hansson void sdio_signal_irq(struct mmc_host *host)
13368269660SUlf Hansson {
13436d57efbSUlf Hansson host->sdio_irq_pending = true;
135*1dd611a9SHeiner Kallweit schedule_work(&host->sdio_irq_work);
13668269660SUlf Hansson }
13768269660SUlf Hansson EXPORT_SYMBOL_GPL(sdio_signal_irq);
13868269660SUlf Hansson
sdio_irq_thread(void * _host)139d1496c39SNicolas Pitre static int sdio_irq_thread(void *_host)
140d1496c39SNicolas Pitre {
141d1496c39SNicolas Pitre struct mmc_host *host = _host;
1426f4285d1SPierre Ossman unsigned long period, idle_period;
143d1496c39SNicolas Pitre int ret;
144d1496c39SNicolas Pitre
145f8ec806bSPeter Zijlstra sched_set_fifo_low(current);
146d1496c39SNicolas Pitre
147d1496c39SNicolas Pitre /*
148d1496c39SNicolas Pitre * We want to allow for SDIO cards to work even on non SDIO
149d1496c39SNicolas Pitre * aware hosts. One thing that non SDIO host cannot do is
150d1496c39SNicolas Pitre * asynchronous notification of pending SDIO card interrupts
151d1496c39SNicolas Pitre * hence we poll for them in that case.
152d1496c39SNicolas Pitre */
1536f4285d1SPierre Ossman idle_period = msecs_to_jiffies(10);
15417b759afSNicolas Pitre period = (host->caps & MMC_CAP_SDIO_IRQ) ?
1556f4285d1SPierre Ossman MAX_SCHEDULE_TIMEOUT : idle_period;
156d1496c39SNicolas Pitre
157d1496c39SNicolas Pitre pr_debug("%s: IRQ thread started (poll period = %lu jiffies)\n",
158d1496c39SNicolas Pitre mmc_hostname(host), period);
159d1496c39SNicolas Pitre
160d1496c39SNicolas Pitre do {
161d1496c39SNicolas Pitre /*
162d1496c39SNicolas Pitre * We claim the host here on drivers behalf for a couple
163d1496c39SNicolas Pitre * reasons:
164d1496c39SNicolas Pitre *
165d1496c39SNicolas Pitre * 1) it is already needed to retrieve the CCCR_INTx;
166d1496c39SNicolas Pitre * 2) we want the driver(s) to clear the IRQ condition ASAP;
167d1496c39SNicolas Pitre * 3) we need to control the abort condition locally.
168d1496c39SNicolas Pitre *
169d1496c39SNicolas Pitre * Just like traditional hard IRQ handlers, we expect SDIO
170d1496c39SNicolas Pitre * IRQ handlers to be quick and to the point, so that the
171d1496c39SNicolas Pitre * holding of the host lock does not cover too much work
172d1496c39SNicolas Pitre * that doesn't require that lock to be held.
173d1496c39SNicolas Pitre */
1746c0cedd1SAdrian Hunter ret = __mmc_claim_host(host, NULL,
1756c0cedd1SAdrian Hunter &host->sdio_irq_thread_abort);
176d1496c39SNicolas Pitre if (ret)
177d1496c39SNicolas Pitre break;
178bbbc4c4dSNicolas Pitre ret = process_sdio_pending_irqs(host);
179d1496c39SNicolas Pitre mmc_release_host(host);
180d1496c39SNicolas Pitre
181d1496c39SNicolas Pitre /*
182d1496c39SNicolas Pitre * Give other threads a chance to run in the presence of
183e633b7bcSPierre Ossman * errors.
184d1496c39SNicolas Pitre */
185e633b7bcSPierre Ossman if (ret < 0) {
186e633b7bcSPierre Ossman set_current_state(TASK_INTERRUPTIBLE);
187e633b7bcSPierre Ossman if (!kthread_should_stop())
188e633b7bcSPierre Ossman schedule_timeout(HZ);
189e633b7bcSPierre Ossman set_current_state(TASK_RUNNING);
190e633b7bcSPierre Ossman }
191d1496c39SNicolas Pitre
1926f4285d1SPierre Ossman /*
1936f4285d1SPierre Ossman * Adaptive polling frequency based on the assumption
1946f4285d1SPierre Ossman * that an interrupt will be closely followed by more.
1956f4285d1SPierre Ossman * This has a substantial benefit for network devices.
1966f4285d1SPierre Ossman */
1976f4285d1SPierre Ossman if (!(host->caps & MMC_CAP_SDIO_IRQ)) {
1986f4285d1SPierre Ossman if (ret > 0)
1996f4285d1SPierre Ossman period /= 2;
2006f4285d1SPierre Ossman else {
2016f4285d1SPierre Ossman period++;
2026f4285d1SPierre Ossman if (period > idle_period)
2036f4285d1SPierre Ossman period = idle_period;
2046f4285d1SPierre Ossman }
2056f4285d1SPierre Ossman }
2066f4285d1SPierre Ossman
2076fee65cfSRobert P. J. Day set_current_state(TASK_INTERRUPTIBLE);
2089eadcc05SUlf Hansson if (host->caps & MMC_CAP_SDIO_IRQ)
20917b759afSNicolas Pitre host->ops->enable_sdio_irq(host, 1);
210d1496c39SNicolas Pitre if (!kthread_should_stop())
211d1496c39SNicolas Pitre schedule_timeout(period);
2126fee65cfSRobert P. J. Day set_current_state(TASK_RUNNING);
213d1496c39SNicolas Pitre } while (!kthread_should_stop());
214d1496c39SNicolas Pitre
2159eadcc05SUlf Hansson if (host->caps & MMC_CAP_SDIO_IRQ)
21617b759afSNicolas Pitre host->ops->enable_sdio_irq(host, 0);
21717b759afSNicolas Pitre
218d1496c39SNicolas Pitre pr_debug("%s: IRQ thread exiting with code %d\n",
219d1496c39SNicolas Pitre mmc_hostname(host), ret);
220d1496c39SNicolas Pitre
221d1496c39SNicolas Pitre return ret;
222d1496c39SNicolas Pitre }
223d1496c39SNicolas Pitre
sdio_card_irq_get(struct mmc_card * card)224d1496c39SNicolas Pitre static int sdio_card_irq_get(struct mmc_card *card)
225d1496c39SNicolas Pitre {
226d1496c39SNicolas Pitre struct mmc_host *host = card->host;
227d1496c39SNicolas Pitre
228d84075c8SPierre Ossman WARN_ON(!host->claimed);
229d1496c39SNicolas Pitre
230d1496c39SNicolas Pitre if (!host->sdio_irqs++) {
231bf3b5ec6SRussell King if (!(host->caps2 & MMC_CAP2_SDIO_IRQ_NOTHREAD)) {
232d1496c39SNicolas Pitre atomic_set(&host->sdio_irq_thread_abort, 0);
233d1496c39SNicolas Pitre host->sdio_irq_thread =
234bf3b5ec6SRussell King kthread_run(sdio_irq_thread, host,
235bf3b5ec6SRussell King "ksdioirqd/%s", mmc_hostname(host));
236d1496c39SNicolas Pitre if (IS_ERR(host->sdio_irq_thread)) {
237d1496c39SNicolas Pitre int err = PTR_ERR(host->sdio_irq_thread);
238d1496c39SNicolas Pitre host->sdio_irqs--;
239d1496c39SNicolas Pitre return err;
240d1496c39SNicolas Pitre }
241dea67c4eSFu Zhonghui } else if (host->caps & MMC_CAP_SDIO_IRQ) {
242bf3b5ec6SRussell King host->ops->enable_sdio_irq(host, 1);
243bf3b5ec6SRussell King }
244d1496c39SNicolas Pitre }
245d1496c39SNicolas Pitre
246d1496c39SNicolas Pitre return 0;
247d1496c39SNicolas Pitre }
248d1496c39SNicolas Pitre
sdio_card_irq_put(struct mmc_card * card)249d1496c39SNicolas Pitre static int sdio_card_irq_put(struct mmc_card *card)
250d1496c39SNicolas Pitre {
251d1496c39SNicolas Pitre struct mmc_host *host = card->host;
252d1496c39SNicolas Pitre
253d84075c8SPierre Ossman WARN_ON(!host->claimed);
2545df0e823SShawn Lin
2555df0e823SShawn Lin if (host->sdio_irqs < 1)
2565df0e823SShawn Lin return -EINVAL;
257d1496c39SNicolas Pitre
258d1496c39SNicolas Pitre if (!--host->sdio_irqs) {
259bf3b5ec6SRussell King if (!(host->caps2 & MMC_CAP2_SDIO_IRQ_NOTHREAD)) {
260d1496c39SNicolas Pitre atomic_set(&host->sdio_irq_thread_abort, 1);
261d1496c39SNicolas Pitre kthread_stop(host->sdio_irq_thread);
262dea67c4eSFu Zhonghui } else if (host->caps & MMC_CAP_SDIO_IRQ) {
263bf3b5ec6SRussell King host->ops->enable_sdio_irq(host, 0);
264bf3b5ec6SRussell King }
265d1496c39SNicolas Pitre }
266d1496c39SNicolas Pitre
267d1496c39SNicolas Pitre return 0;
268d1496c39SNicolas Pitre }
269d1496c39SNicolas Pitre
27006e8935fSStefan Nilsson XK /* If there is only 1 function registered set sdio_single_irq */
sdio_single_irq_set(struct mmc_card * card)27106e8935fSStefan Nilsson XK static void sdio_single_irq_set(struct mmc_card *card)
27206e8935fSStefan Nilsson XK {
27306e8935fSStefan Nilsson XK struct sdio_func *func;
27406e8935fSStefan Nilsson XK int i;
27506e8935fSStefan Nilsson XK
27606e8935fSStefan Nilsson XK card->sdio_single_irq = NULL;
27706e8935fSStefan Nilsson XK if ((card->host->caps & MMC_CAP_SDIO_IRQ) &&
2786660d0aeSJérôme Pouiller card->host->sdio_irqs == 1) {
27906e8935fSStefan Nilsson XK for (i = 0; i < card->sdio_funcs; i++) {
28006e8935fSStefan Nilsson XK func = card->sdio_func[i];
28106e8935fSStefan Nilsson XK if (func && func->irq_handler) {
28206e8935fSStefan Nilsson XK card->sdio_single_irq = func;
28306e8935fSStefan Nilsson XK break;
28406e8935fSStefan Nilsson XK }
28506e8935fSStefan Nilsson XK }
28606e8935fSStefan Nilsson XK }
2876660d0aeSJérôme Pouiller }
28806e8935fSStefan Nilsson XK
289d1496c39SNicolas Pitre /**
290d1496c39SNicolas Pitre * sdio_claim_irq - claim the IRQ for a SDIO function
291d1496c39SNicolas Pitre * @func: SDIO function
292d1496c39SNicolas Pitre * @handler: IRQ handler callback
293d1496c39SNicolas Pitre *
294d1496c39SNicolas Pitre * Claim and activate the IRQ for the given SDIO function. The provided
295d1496c39SNicolas Pitre * handler will be called when that IRQ is asserted. The host is always
2968647d26eSJoel Cunningham * claimed already when the handler is called so the handler should not
2978647d26eSJoel Cunningham * call sdio_claim_host() or sdio_release_host().
298d1496c39SNicolas Pitre */
sdio_claim_irq(struct sdio_func * func,sdio_irq_handler_t * handler)299d1496c39SNicolas Pitre int sdio_claim_irq(struct sdio_func *func, sdio_irq_handler_t *handler)
300d1496c39SNicolas Pitre {
301d1496c39SNicolas Pitre int ret;
302d1496c39SNicolas Pitre unsigned char reg;
303d1496c39SNicolas Pitre
3045df0e823SShawn Lin if (!func)
3055df0e823SShawn Lin return -EINVAL;
306d1496c39SNicolas Pitre
307d1496c39SNicolas Pitre pr_debug("SDIO: Enabling IRQ for %s...\n", sdio_func_id(func));
308d1496c39SNicolas Pitre
309d1496c39SNicolas Pitre if (func->irq_handler) {
310d1496c39SNicolas Pitre pr_debug("SDIO: IRQ for %s already in use.\n", sdio_func_id(func));
311d1496c39SNicolas Pitre return -EBUSY;
312d1496c39SNicolas Pitre }
313d1496c39SNicolas Pitre
314d1496c39SNicolas Pitre ret = mmc_io_rw_direct(func->card, 0, 0, SDIO_CCCR_IENx, 0, ®);
315d1496c39SNicolas Pitre if (ret)
316d1496c39SNicolas Pitre return ret;
317d1496c39SNicolas Pitre
318d1496c39SNicolas Pitre reg |= 1 << func->num;
319d1496c39SNicolas Pitre
320d1496c39SNicolas Pitre reg |= 1; /* Master interrupt enable */
321d1496c39SNicolas Pitre
322d1496c39SNicolas Pitre ret = mmc_io_rw_direct(func->card, 1, 0, SDIO_CCCR_IENx, reg, NULL);
323d1496c39SNicolas Pitre if (ret)
324d1496c39SNicolas Pitre return ret;
325d1496c39SNicolas Pitre
326d1496c39SNicolas Pitre func->irq_handler = handler;
327d1496c39SNicolas Pitre ret = sdio_card_irq_get(func->card);
328d1496c39SNicolas Pitre if (ret)
329d1496c39SNicolas Pitre func->irq_handler = NULL;
33006e8935fSStefan Nilsson XK sdio_single_irq_set(func->card);
331d1496c39SNicolas Pitre
332d1496c39SNicolas Pitre return ret;
333d1496c39SNicolas Pitre }
334d1496c39SNicolas Pitre EXPORT_SYMBOL_GPL(sdio_claim_irq);
335d1496c39SNicolas Pitre
336d1496c39SNicolas Pitre /**
337d1496c39SNicolas Pitre * sdio_release_irq - release the IRQ for a SDIO function
338d1496c39SNicolas Pitre * @func: SDIO function
339d1496c39SNicolas Pitre *
340d1496c39SNicolas Pitre * Disable and release the IRQ for the given SDIO function.
341d1496c39SNicolas Pitre */
sdio_release_irq(struct sdio_func * func)342d1496c39SNicolas Pitre int sdio_release_irq(struct sdio_func *func)
343d1496c39SNicolas Pitre {
344d1496c39SNicolas Pitre int ret;
345d1496c39SNicolas Pitre unsigned char reg;
346d1496c39SNicolas Pitre
3475df0e823SShawn Lin if (!func)
3485df0e823SShawn Lin return -EINVAL;
349d1496c39SNicolas Pitre
350d1496c39SNicolas Pitre pr_debug("SDIO: Disabling IRQ for %s...\n", sdio_func_id(func));
351d1496c39SNicolas Pitre
352d1496c39SNicolas Pitre if (func->irq_handler) {
353d1496c39SNicolas Pitre func->irq_handler = NULL;
354d1496c39SNicolas Pitre sdio_card_irq_put(func->card);
35506e8935fSStefan Nilsson XK sdio_single_irq_set(func->card);
356d1496c39SNicolas Pitre }
357d1496c39SNicolas Pitre
358d1496c39SNicolas Pitre ret = mmc_io_rw_direct(func->card, 0, 0, SDIO_CCCR_IENx, 0, ®);
359d1496c39SNicolas Pitre if (ret)
360d1496c39SNicolas Pitre return ret;
361d1496c39SNicolas Pitre
362d1496c39SNicolas Pitre reg &= ~(1 << func->num);
363d1496c39SNicolas Pitre
364d1496c39SNicolas Pitre /* Disable master interrupt with the last function interrupt */
365d1496c39SNicolas Pitre if (!(reg & 0xFE))
366d1496c39SNicolas Pitre reg = 0;
367d1496c39SNicolas Pitre
368d1496c39SNicolas Pitre ret = mmc_io_rw_direct(func->card, 1, 0, SDIO_CCCR_IENx, reg, NULL);
369d1496c39SNicolas Pitre if (ret)
370d1496c39SNicolas Pitre return ret;
371d1496c39SNicolas Pitre
372d1496c39SNicolas Pitre return 0;
373d1496c39SNicolas Pitre }
374d1496c39SNicolas Pitre EXPORT_SYMBOL_GPL(sdio_release_irq);
375d1496c39SNicolas Pitre
376