source: mainline/uspace/drv/block/pci-ide/pci-ide.c@ a38d504

Last change on this file since a38d504 was a38d504, checked in by Jiri Svoboda <jiri@…>, 18 months ago

Limit I/O operations not to exceed DMA buffer size

  • Property mode set to 100644
File size: 16.8 KB
Line 
1/*
2 * Copyright (c) 2024 Jiri Svoboda
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * - Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 * - Redistributions in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
14 * - The name of the author may not be used to endorse or promote products
15 * derived from this software without specific prior written permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
18 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
19 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
21 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
22 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 */
28
29/** @addtogroup pci-ide
30 * @{
31 */
32
33/**
34 * @file
35 * @brief PCI IDE driver
36 */
37
38#include <assert.h>
39#include <ddi.h>
40#include <ddf/interrupt.h>
41#include <ddf/log.h>
42#include <async.h>
43#include <as.h>
44#include <fibril_synch.h>
45#include <stdint.h>
46#include <stdbool.h>
47#include <stdio.h>
48#include <stddef.h>
49#include <str.h>
50#include <inttypes.h>
51#include <errno.h>
52
53#include "pci-ide.h"
54#include "main.h"
55
56static errno_t pci_ide_init_io(pci_ide_channel_t *);
57static void pci_ide_fini_io(pci_ide_channel_t *);
58static errno_t pci_ide_init_irq(pci_ide_channel_t *);
59static void pci_ide_fini_irq(pci_ide_channel_t *);
60static void pci_ide_irq_handler(ipc_call_t *, void *);
61
62static void pci_ide_write_data_16(void *, uint16_t *, size_t);
63static void pci_ide_read_data_16(void *, uint16_t *, size_t);
64static void pci_ide_write_cmd_8(void *, uint16_t, uint8_t);
65static uint8_t pci_ide_read_cmd_8(void *, uint16_t);
66static void pci_ide_write_ctl_8(void *, uint16_t, uint8_t);
67static uint8_t pci_ide_read_ctl_8(void *, uint16_t);
68static errno_t pci_ide_irq_enable(void *);
69static errno_t pci_ide_irq_disable(void *);
70static void pci_ide_dma_chan_setup(void *, void *, size_t, ata_dma_dir_t);
71static void pci_ide_dma_chan_teardown(void *);
72static errno_t pci_ide_add_device(void *, unsigned, void *);
73static errno_t pci_ide_remove_device(void *, unsigned);
74static void pci_ide_msg_debug(void *, char *);
75static void pci_ide_msg_note(void *, char *);
76static void pci_ide_msg_warn(void *, char *);
77static void pci_ide_msg_error(void *, char *);
78
79static const irq_pio_range_t pci_ide_irq_ranges[] = {
80 {
81 .base = 0,
82 .size = sizeof(ata_cmd_t)
83 }
84};
85
86/** IDE interrupt pseudo code. */
87static const irq_cmd_t pci_ide_irq_cmds[] = {
88 {
89 .cmd = CMD_PIO_READ_8,
90 .addr = NULL, /* will be patched in run-time */
91 .dstarg = 1
92 },
93 {
94 .cmd = CMD_ACCEPT
95 }
96};
97
98/** Initialize PCI IDE controller.
99 *
100 * @param ctrl PCI IDE controller
101 * @param res Hardware resources
102 *
103 * @return EOK on success or an error code
104 */
105errno_t pci_ide_ctrl_init(pci_ide_ctrl_t *ctrl, pci_ide_hwres_t *res)
106{
107 errno_t rc;
108 void *vaddr;
109
110 ddf_msg(LVL_DEBUG, "pci_ide_ctrl_init()");
111 ctrl->bmregs_physical = res->bmregs;
112
113 ddf_msg(LVL_NOTE, "Bus master IDE regs I/O address: 0x%lx",
114 ctrl->bmregs_physical);
115
116 rc = pio_enable((void *)ctrl->bmregs_physical, sizeof(pci_ide_regs_t),
117 &vaddr);
118 if (rc != EOK) {
119 ddf_msg(LVL_ERROR, "Cannot initialize device I/O space.");
120 return rc;
121 }
122
123 ctrl->bmregs = vaddr;
124
125 ddf_msg(LVL_DEBUG, "pci_ide_ctrl_init: DONE");
126 return EOK;
127}
128
129/** Finalize PCI IDE controller.
130 *
131 * @param ctrl PCI IDE controller
132 * @return EOK on success or an error code
133 */
134errno_t pci_ide_ctrl_fini(pci_ide_ctrl_t *ctrl)
135{
136 ddf_msg(LVL_DEBUG, ": pci_ide_ctrl_fini()");
137
138 // XXX TODO
139 return EOK;
140}
141
142/** Initialize PCI IDE channel. */
143errno_t pci_ide_channel_init(pci_ide_ctrl_t *ctrl, pci_ide_channel_t *chan,
144 unsigned chan_id, pci_ide_hwres_t *res)
145{
146 errno_t rc;
147 bool irq_inited = false;
148 ata_params_t params;
149 void *buffer;
150
151 ddf_msg(LVL_DEBUG, "pci_ide_channel_init()");
152
153 chan->ctrl = ctrl;
154 chan->chan_id = chan_id;
155 fibril_mutex_initialize(&chan->lock);
156 if (chan_id == 0) {
157 chan->cmd_physical = res->cmd1;
158 chan->ctl_physical = res->ctl1;
159 chan->irq = res->irq1;
160 } else {
161 chan->cmd_physical = res->cmd2;
162 chan->ctl_physical = res->ctl2;
163 chan->irq = res->irq2;
164 }
165
166 chan->dma_buf_size = 8192;
167
168 ddf_msg(LVL_NOTE, "I/O address %p/%p", (void *) chan->cmd_physical,
169 (void *) chan->ctl_physical);
170
171 ddf_msg(LVL_DEBUG, "Init I/O");
172 rc = pci_ide_init_io(chan);
173 if (rc != EOK)
174 return rc;
175
176 ddf_msg(LVL_DEBUG, "Init IRQ");
177 rc = pci_ide_init_irq(chan);
178 if (rc != EOK) {
179 ddf_msg(LVL_NOTE, "init IRQ failed");
180 goto error;
181 }
182
183 irq_inited = true;
184
185 ddf_msg(LVL_DEBUG, "Allocate PRD table");
186
187 buffer = AS_AREA_ANY;
188 rc = dmamem_map_anonymous(sizeof (pci_ide_prd_t), DMAMEM_4GiB | 0xffff,
189 AS_AREA_WRITE | AS_AREA_READ, 0, &chan->prdt_pa, &buffer);
190 if (rc != EOK) {
191 ddf_msg(LVL_NOTE, "Failed allocating PRD table.");
192 goto error;
193 }
194
195 chan->prdt = (pci_ide_prd_t *)buffer;
196
197 ddf_msg(LVL_DEBUG, "Allocate DMA buffer");
198
199 buffer = AS_AREA_ANY;
200 rc = dmamem_map_anonymous(chan->dma_buf_size, DMAMEM_4GiB | 0xffff,
201 AS_AREA_WRITE | AS_AREA_READ, 0, &chan->dma_buf_pa, &buffer);
202 if (rc != EOK) {
203 ddf_msg(LVL_NOTE, "Failed allocating PRD table.");
204 goto error;
205 }
206
207 chan->dma_buf = buffer;
208
209 /* Populate PRD with information on our single DMA buffer */
210 chan->prdt->pba = host2uint32_t_le(chan->dma_buf_pa);
211 chan->prdt->bcnt = host2uint16_t_le(chan->dma_buf_size);
212 chan->prdt->eot_res = host2uint16_t_le(pci_ide_prd_eot);
213
214 /* Program PRD table pointer register */
215 if (chan_id == 0) {
216 pio_write_32(&chan->ctrl->bmregs->bmidtpp,
217 (uint32_t)chan->prdt_pa);
218 } else {
219 pio_write_32(&chan->ctrl->bmregs->bmidtps,
220 (uint32_t)chan->prdt_pa);
221 }
222
223 ddf_msg(LVL_DEBUG, "pci_ide_channel_init(): Initialize IDE channel");
224
225 params.arg = (void *)chan;
226 params.have_irq = (chan->irq >= 0) ? true : false;
227 params.use_dma = true;
228 params.max_dma_xfer = chan->dma_buf_size;
229 params.write_data_16 = pci_ide_write_data_16;
230 params.read_data_16 = pci_ide_read_data_16;
231 params.write_cmd_8 = pci_ide_write_cmd_8;
232 params.read_cmd_8 = pci_ide_read_cmd_8;
233 params.write_ctl_8 = pci_ide_write_ctl_8;
234 params.read_ctl_8 = pci_ide_read_ctl_8;
235 params.irq_enable = pci_ide_irq_enable;
236 params.irq_disable = pci_ide_irq_disable;
237 params.dma_chan_setup = pci_ide_dma_chan_setup;
238 params.dma_chan_teardown = pci_ide_dma_chan_teardown;
239 params.add_device = pci_ide_add_device;
240 params.remove_device = pci_ide_remove_device;
241 params.msg_debug = pci_ide_msg_debug;
242 params.msg_note = pci_ide_msg_note;
243 params.msg_warn = pci_ide_msg_warn;
244 params.msg_error = pci_ide_msg_error;
245
246 rc = ata_channel_create(&params, &chan->channel);
247 if (rc != EOK)
248 goto error;
249
250 rc = ata_channel_initialize(chan->channel);
251 if (rc != EOK)
252 goto error;
253
254 ddf_msg(LVL_DEBUG, "pci_ide_channel_init: DONE");
255 return EOK;
256error:
257 if (irq_inited)
258 pci_ide_fini_irq(chan);
259 pci_ide_fini_io(chan);
260 return rc;
261}
262
263/** Finalize PCI IDE channel. */
264errno_t pci_ide_channel_fini(pci_ide_channel_t *chan)
265{
266 errno_t rc;
267
268 ddf_msg(LVL_DEBUG, ": pci_ide_channel_fini()");
269
270 fibril_mutex_lock(&chan->lock);
271
272 rc = ata_channel_destroy(chan->channel);
273 if (rc != EOK) {
274 fibril_mutex_unlock(&chan->lock);
275 return rc;
276 }
277
278 pci_ide_fini_irq(chan);
279 pci_ide_fini_io(chan);
280 fibril_mutex_unlock(&chan->lock);
281
282 return EOK;
283}
284
285/** Enable device I/O. */
286static errno_t pci_ide_init_io(pci_ide_channel_t *chan)
287{
288 errno_t rc;
289 void *vaddr;
290
291 rc = pio_enable((void *) chan->cmd_physical, sizeof(ata_cmd_t), &vaddr);
292 if (rc != EOK) {
293 ddf_msg(LVL_ERROR, "Cannot initialize device I/O space.");
294 return rc;
295 }
296
297 chan->cmd = vaddr;
298
299 rc = pio_enable((void *) chan->ctl_physical, sizeof(ata_ctl_t), &vaddr);
300 if (rc != EOK) {
301 ddf_msg(LVL_ERROR, "Cannot initialize device I/O space.");
302 return rc;
303 }
304
305 chan->ctl = vaddr;
306
307 rc = pio_enable((void *) chan->cmd_physical, sizeof(ata_cmd_t), &vaddr);
308 if (rc != EOK) {
309 ddf_msg(LVL_ERROR, "Cannot initialize device I/O space.");
310 return rc;
311 }
312
313 chan->cmd = vaddr;
314
315 return EOK;
316}
317
318/** Clean up device I/O. */
319static void pci_ide_fini_io(pci_ide_channel_t *chan)
320{
321 (void) chan;
322 /* XXX TODO */
323}
324
325/** Initialize IRQ. */
326static errno_t pci_ide_init_irq(pci_ide_channel_t *chan)
327{
328 irq_code_t irq_code;
329 irq_pio_range_t *ranges;
330 irq_cmd_t *cmds;
331 errno_t rc;
332
333 if (chan->irq < 0)
334 return EOK;
335
336 ranges = malloc(sizeof(pci_ide_irq_ranges));
337 if (ranges == NULL)
338 return ENOMEM;
339
340 cmds = malloc(sizeof(pci_ide_irq_cmds));
341 if (cmds == NULL) {
342 free(cmds);
343 return ENOMEM;
344 }
345
346 memcpy(ranges, &pci_ide_irq_ranges, sizeof(pci_ide_irq_ranges));
347 ranges[0].base = chan->cmd_physical;
348 memcpy(cmds, &pci_ide_irq_cmds, sizeof(pci_ide_irq_cmds));
349 cmds[0].addr = &chan->cmd->status;
350
351 irq_code.rangecount = sizeof(pci_ide_irq_ranges) / sizeof(irq_pio_range_t);
352 irq_code.ranges = ranges;
353 irq_code.cmdcount = sizeof(pci_ide_irq_cmds) / sizeof(irq_cmd_t);
354 irq_code.cmds = cmds;
355
356 ddf_msg(LVL_NOTE, "IRQ %d", chan->irq);
357 rc = register_interrupt_handler(chan->ctrl->dev, chan->irq,
358 pci_ide_irq_handler, (void *)chan, &irq_code, &chan->ihandle);
359 if (rc != EOK) {
360 ddf_msg(LVL_ERROR, "Error registering IRQ.");
361 goto error;
362 }
363
364 ddf_msg(LVL_DEBUG, "Interrupt handler registered");
365 free(ranges);
366 free(cmds);
367 return EOK;
368error:
369 free(ranges);
370 free(cmds);
371 return rc;
372}
373
374/** Clean up IRQ. */
375static void pci_ide_fini_irq(pci_ide_channel_t *chan)
376{
377 errno_t rc;
378 async_sess_t *parent_sess;
379
380 parent_sess = ddf_dev_parent_sess_get(chan->ctrl->dev);
381
382 rc = hw_res_disable_interrupt(parent_sess, chan->irq);
383 if (rc != EOK)
384 ddf_msg(LVL_ERROR, "Error disabling IRQ.");
385
386 (void) unregister_interrupt_handler(chan->ctrl->dev, chan->ihandle);
387}
388
389/** Interrupt handler.
390 *
391 * @param call Call data
392 * @param arg Argument (pci_ide_channel_t *)
393 */
394static void pci_ide_irq_handler(ipc_call_t *call, void *arg)
395{
396 pci_ide_channel_t *chan = (pci_ide_channel_t *)arg;
397 uint8_t status;
398 async_sess_t *parent_sess;
399
400 status = ipc_get_arg1(call);
401 ata_channel_irq(chan->channel, status);
402
403 parent_sess = ddf_dev_parent_sess_get(chan->ctrl->dev);
404 hw_res_clear_interrupt(parent_sess, chan->irq);
405}
406
407/** Write the data register callback handler.
408 *
409 * @param arg Argument (pci_ide_channel_t *)
410 * @param data Data
411 * @param nwords Number of words to write
412 */
413static void pci_ide_write_data_16(void *arg, uint16_t *data, size_t nwords)
414{
415 pci_ide_channel_t *chan = (pci_ide_channel_t *)arg;
416 size_t i;
417
418 for (i = 0; i < nwords; i++)
419 pio_write_16(&chan->cmd->data_port, data[i]);
420}
421
422/** Read the data register callback handler.
423 *
424 * @param arg Argument (pci_ide_channel_t *)
425 * @param buf Destination buffer
426 * @param nwords Number of words to read
427 */
428static void pci_ide_read_data_16(void *arg, uint16_t *buf, size_t nwords)
429{
430 pci_ide_channel_t *chan = (pci_ide_channel_t *)arg;
431 size_t i;
432
433 for (i = 0; i < nwords; i++)
434 buf[i] = pio_read_16(&chan->cmd->data_port);
435}
436
437/** Write command register callback handler.
438 *
439 * @param arg Argument (pci_ide_channel_t *)
440 * @param off Register offset
441 * @param value Value to write to command register
442 */
443static void pci_ide_write_cmd_8(void *arg, uint16_t off, uint8_t value)
444{
445 pci_ide_channel_t *chan = (pci_ide_channel_t *)arg;
446
447 pio_write_8(((ioport8_t *)chan->cmd) + off, value);
448}
449
450/** Read command register callback handler.
451 *
452 * @param arg Argument (pci_ide_channel_t *)
453 * @param off Register offset
454 * @return value Value read from command register
455 */
456static uint8_t pci_ide_read_cmd_8(void *arg, uint16_t off)
457{
458 pci_ide_channel_t *chan = (pci_ide_channel_t *)arg;
459
460 return pio_read_8(((ioport8_t *)chan->cmd) + off);
461}
462
463/** Write control register callback handler.
464 *
465 * @param arg Argument (pci_ide_channel_t *)
466 * @param off Register offset
467 * @param value Value to write to control register
468 */
469static void pci_ide_write_ctl_8(void *arg, uint16_t off, uint8_t value)
470{
471 pci_ide_channel_t *chan = (pci_ide_channel_t *)arg;
472
473 pio_write_8(((ioport8_t *)chan->ctl) + off, value);
474}
475
476/** Read control register callback handler.
477 *
478 * @param arg Argument (pci_ide_channel_t *)
479 * @param off Register offset
480 * @return value Value read from control register
481 */
482static uint8_t pci_ide_read_ctl_8(void *arg, uint16_t off)
483{
484 pci_ide_channel_t *chan = (pci_ide_channel_t *)arg;
485
486 return pio_read_8(((ioport8_t *)chan->ctl) + off);
487}
488
489/** Enable IRQ callback handler
490 *
491 * @param arg Argument (pci_ide_channel_t *)
492 * @return EOK on success or an error code
493 */
494static errno_t pci_ide_irq_enable(void *arg)
495{
496 pci_ide_channel_t *chan = (pci_ide_channel_t *)arg;
497 async_sess_t *parent_sess;
498 errno_t rc;
499
500 ddf_msg(LVL_DEBUG, "Enable IRQ %d for channel %u",
501 chan->irq, chan->chan_id);
502
503 parent_sess = ddf_dev_parent_sess_get(chan->ctrl->dev);
504
505 rc = hw_res_enable_interrupt(parent_sess, chan->irq);
506 if (rc != EOK) {
507 ddf_msg(LVL_ERROR, "Error enabling IRQ.");
508 return rc;
509 }
510
511 return EOK;
512}
513
514/** Disable IRQ callback handler
515 *
516 * @param arg Argument (pci_ide_channel_t *)
517 * @return EOK on success or an error code
518 */
519static errno_t pci_ide_irq_disable(void *arg)
520{
521 pci_ide_channel_t *chan = (pci_ide_channel_t *)arg;
522 async_sess_t *parent_sess;
523 errno_t rc;
524
525 ddf_msg(LVL_DEBUG, "Disable IRQ");
526
527 parent_sess = ddf_dev_parent_sess_get(chan->ctrl->dev);
528
529 rc = hw_res_disable_interrupt(parent_sess, chan->irq);
530 if (rc != EOK) {
531 ddf_msg(LVL_ERROR, "Error disabling IRQ.");
532 return rc;
533 }
534
535 return EOK;
536}
537
538/** Set up DMA channel callback handler
539 *
540 * @param arg Argument (pci_ide_channel_t *)
541 * @param buf Buffer
542 * @param buf_size Buffer size
543 * @param dir DMA transfer direction
544 */
545static void pci_ide_dma_chan_setup(void *arg, void *buf, size_t buf_size,
546 ata_dma_dir_t dir)
547{
548 pci_ide_channel_t *chan = (pci_ide_channel_t *)arg;
549 uint8_t *bmicx;
550 uint8_t val;
551
552 /* Needed later in teardown */
553 chan->cur_dir = dir;
554 chan->cur_buf = buf;
555 chan->cur_buf_size = buf_size;
556
557 if (dir == ata_dma_write) {
558 assert(buf_size < chan->dma_buf_size);
559 memcpy(chan->dma_buf, buf, buf_size);
560 }
561
562 /* Primary or secondary channel control register */
563 bmicx = (chan->chan_id == 0) ? &chan->ctrl->bmregs->bmicp :
564 &chan->ctrl->bmregs->bmics;
565
566 /* Set read / write */
567 val = (dir == ata_dma_write) ? bmicx_rwcon : 0;
568 pio_write_8(bmicx, val);
569
570 /* Start bus master DMA engine */
571 val = val | bmicx_ssbm;
572 pio_write_8(bmicx, val);
573}
574
575/** Tear down DMA channel callback handler
576 *
577 * @param arg Argument (pci_ide_channel_t *)
578 */
579static void pci_ide_dma_chan_teardown(void *arg)
580{
581 pci_ide_channel_t *chan = (pci_ide_channel_t *)arg;
582 uint8_t *bmicx;
583 uint8_t val;
584
585 /* Primary or secondary channel control register */
586 bmicx = (chan->chan_id == 0) ? &chan->ctrl->bmregs->bmicp :
587 &chan->ctrl->bmregs->bmics;
588
589 /* Stop bus master DMA engine clear SSBM but keep RWCON the same */
590 val = (chan->cur_dir == ata_dma_write) ? bmicx_rwcon : 0;
591 pio_write_8(bmicx, val);
592
593 if (chan->cur_dir == ata_dma_read) {
594 assert(chan->cur_buf_size < chan->dma_buf_size);
595 memcpy(chan->cur_buf, chan->dma_buf, chan->cur_buf_size);
596 }
597}
598
599/** Add ATA device callback handler.
600 *
601 * @param arg Argument (pci_ide_channel_t *)
602 * @param idx Device index
603 * $param charg Connection handler argument
604 * @return EOK on success or an error code
605 */
606static errno_t pci_ide_add_device(void *arg, unsigned idx, void *charg)
607{
608 pci_ide_channel_t *chan = (pci_ide_channel_t *)arg;
609 return pci_ide_fun_create(chan, idx, charg);
610}
611
612/** Remove ATA device callback handler.
613 *
614 * @param arg Argument (pci_ide_channel_t *)
615 * @param idx Device index
616 * @return EOK on success or an error code
617 */
618static errno_t pci_ide_remove_device(void *arg, unsigned idx)
619{
620 pci_ide_channel_t *chan = (pci_ide_channel_t *)arg;
621 return pci_ide_fun_remove(chan, idx);
622}
623
624/** Debug message callback handler.
625 *
626 * @param arg Argument (pci_ide_channel_t *)
627 * @param msg Message
628 */
629static void pci_ide_msg_debug(void *arg, char *msg)
630{
631 (void)arg;
632 ddf_msg(LVL_DEBUG, "%s", msg);
633}
634
635/** Notice message callback handler.
636 *
637 * @param arg Argument (pci_ide_channel_t *)
638 * @param msg Message
639 */
640static void pci_ide_msg_note(void *arg, char *msg)
641{
642 (void)arg;
643 ddf_msg(LVL_NOTE, "%s", msg);
644}
645
646/** Warning message callback handler.
647 *
648 * @param arg Argument (pci_ide_channel_t *)
649 * @param msg Message
650 */
651static void pci_ide_msg_warn(void *arg, char *msg)
652{
653 (void)arg;
654 ddf_msg(LVL_WARN, "%s", msg);
655}
656
657/** Error message callback handler.
658 *
659 * @param arg Argument (pci_ide_channel_t *)
660 * @param msg Message
661 */
662static void pci_ide_msg_error(void *arg, char *msg)
663{
664 (void)arg;
665 ddf_msg(LVL_ERROR, "%s", msg);
666}
667
668/**
669 * @}
670 */
Note: See TracBrowser for help on using the repository browser.