source: trunk/hal/tsar_mips32/drivers/soclib_tty.c @ 658

Last change on this file since 658 was 658, checked in by alain, 4 years ago

Improve the TSAR NIC driver.

File size: 20.5 KB
Line 
1/*
2 * soclib_tty.c - soclib tty driver implementation.
3 *
4 * Author  Alain Greiner (2016,2017,2018,2019,2020)
5 *
6 * Copyright (c)  UPMC Sorbonne Universites
7 *
8 * This file is part of ALMOS-MKH.
9 *
10 * ALMOS-MKH. is free software; you can redistribute it and/or modify it
11 * under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; version 2.0 of the License.
13 *
14 * ALMOS-MKH is distributed in the hope that it will be useful, but
15 * WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17 * General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with ALMOS-MKH; if not, write to the Free Software Foundation,
21 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
22 */
23
24
25#include <hal_kernel_types.h>
26#include <dev_txt.h>
27#include <chdev.h>
28#include <soclib_tty.h>
29#include <thread.h>
30#include <printk.h>
31#include <hal_special.h>
32
33#if (DEBUG_SYS_READ & 1)
34extern uint32_t  enter_tty_cmd_read;
35extern uint32_t  exit_tty_cmd_read;
36
37extern uint32_t  enter_tty_isr_read;
38extern uint32_t  exit_tty_isr_read;
39#endif
40
41#if (DEBUG_SYS_WRITE & 1)
42extern uint32_t  enter_tty_cmd_write;
43extern uint32_t  exit_tty_cmd_write;
44
45extern uint32_t  enter_tty_isr_write;
46extern uint32_t  exit_tty_isr_write;
47#endif
48
49////////////////////////////////////////////////////////////////////////////////////
50// These global variables implement the TTY_RX  FIFOs (one per channel)
51////////////////////////////////////////////////////////////////////////////////////
52// Implementation note:
53// We allocate - in each cluster - two arrays of FIFOs containing as many entries
54// as the total number of TXT channels, but all entries are not used in all
55// clusters: for a given cluster K, a given entry corresponding to a given channel
56// and a given direction is only used if the associated chdev is in cluster K.
57// With this policy, the driver can ignore the actual placement of chdevs.
58////////////////////////////////////////////////////////////////////////////////////
59
60__attribute__((section(".kdata")))
61tty_fifo_t  tty_rx_fifo[CONFIG_MAX_TXT_CHANNELS];
62
63__attribute__((section(".kdata")))
64tty_fifo_t  tty_tx_fifo[CONFIG_MAX_TXT_CHANNELS];
65
66///////////////////////////////////////
67void soclib_tty_init( chdev_t * chdev )
68{
69    xptr_t reg_xp;
70
71    // initialise function pointers in chdev
72    chdev->cmd = &soclib_tty_cmd;
73    chdev->isr = &soclib_tty_isr;
74    chdev->aux = &soclib_tty_aux;
75
76    // get TTY channel and extended pointer on TTY peripheral base address
77    xptr_t   tty_xp  = chdev->base;
78    uint32_t channel = chdev->channel;
79    bool_t   is_rx   = chdev->is_rx;
80
81    // get SOCLIB_TTY device cluster and local pointer
82    cxy_t      tty_cxy = GET_CXY( tty_xp );
83    uint32_t * tty_ptr = GET_PTR( tty_xp );
84
85    // set TTY_RX_IRQ_ENABLE
86    reg_xp = XPTR( tty_cxy , tty_ptr + (channel * TTY_SPAN) + TTY_RX_IRQ_ENABLE );
87    hal_remote_s32( reg_xp , 1 );
88
89    // reset TTY_TX_IRQ_ENABLE
90    reg_xp = XPTR( tty_cxy , tty_ptr + (channel * TTY_SPAN) + TTY_TX_IRQ_ENABLE );
91    hal_remote_s32( reg_xp , 0 );
92
93    // reset relevant FIFO
94    if( is_rx )
95    {
96        tty_rx_fifo[channel].sts = 0;
97        tty_rx_fifo[channel].ptr = 0;
98        tty_rx_fifo[channel].ptw = 0;
99    }
100    else
101    {
102        tty_tx_fifo[channel].sts = 0;
103        tty_tx_fifo[channel].ptr = 0;
104        tty_tx_fifo[channel].ptw = 0;
105    }
106}  // end soclib_tty_init()
107
108//////////////////////////////////////////////////////////////
109void __attribute__ ((noinline)) soclib_tty_cmd( xptr_t th_xp )
110{
111    tty_fifo_t * fifo;     // TTY_RX or TTY_TX FIFO
112    char         byte;     // byte value
113    uint32_t     done;     // number of bytes moved
114
115    // get client thread cluster and local pointer
116    cxy_t      th_cxy = GET_CXY( th_xp );
117    thread_t * th_ptr = GET_PTR( th_xp );
118
119    // get command arguments
120    uint32_t type     = hal_remote_l32 ( XPTR( th_cxy , &th_ptr->txt_cmd.type   ) );
121    xptr_t   buf_xp   = hal_remote_l64( XPTR( th_cxy , &th_ptr->txt_cmd.buf_xp ) );
122    uint32_t count    = hal_remote_l32 ( XPTR( th_cxy , &th_ptr->txt_cmd.count  ) );
123    xptr_t   error_xp = XPTR( th_cxy , &th_ptr->txt_cmd.error );
124
125#if (DEBUG_SYS_READ & 1)
126if( type == TXT_READ) enter_tty_cmd_read = (uint32_t)hal_get_cycles();
127#endif
128
129#if (DEBUG_SYS_WRITE & 1)
130if( type == TXT_WRITE) enter_tty_cmd_write = (uint32_t)hal_get_cycles();
131#endif
132
133#if( DEBUG_HAL_TXT_TX || DEBUG_HAL_TXT_RX )
134thread_t * this = CURRENT_THREAD;
135#endif
136
137    // get TXT device cluster and pointers
138    xptr_t     dev_xp = (xptr_t)hal_remote_l64( XPTR( th_cxy , &th_ptr->txt_cmd.dev_xp ) );
139    cxy_t      dev_cxy = GET_CXY( dev_xp );
140    chdev_t  * dev_ptr = GET_PTR( dev_xp );
141
142    // get cluster and pointers for SOCLIB_TTY peripheral base segment
143    xptr_t     tty_xp = (xptr_t)hal_remote_l64( XPTR( dev_cxy , &dev_ptr->base ) );
144    cxy_t      tty_cxy = GET_CXY( tty_xp );
145    uint32_t * tty_ptr = GET_PTR( tty_xp );
146
147    // get TTY channel index and channel base address
148    uint32_t   channel = hal_remote_l32( XPTR( dev_cxy , &dev_ptr->channel ) );
149    uint32_t * base    = tty_ptr + TTY_SPAN * channel;
150
151    ///////////////////////
152    if( type == TXT_WRITE )         // write bytes to TTY_TX FIFO
153    {
154        fifo = &tty_tx_fifo[channel];
155
156        done = 0;
157
158        while( done < count )
159        {
160            if( fifo->sts < TTY_FIFO_DEPTH )   // put one byte to FIFO if TX_FIFO not full
161            {
162                // get one byte from command buffer
163                byte = hal_remote_lb( buf_xp + done );
164
165#if DEBUG_HAL_TXT_TX
166uint32_t   tx_cycle = (uint32_t)hal_get_cycles();
167if( DEBUG_HAL_TXT_TX < tx_cycle )
168printk("\n[%s] thread[%x,%x] put character <%c> to TXT%d_TX fifo / cycle %d\n",
169__FUNCTION__, this->process->pid, this->trdid, byte, channel, tx_cycle );
170#endif
171                // write byte to FIFO
172                fifo->data[fifo->ptw] = byte;
173
174                // prevent race
175                hal_fence();
176
177                // update FIFO state
178                fifo->ptw = (fifo->ptw + 1) % TTY_FIFO_DEPTH;
179                hal_atomic_add( &fifo->sts , 1 );
180
181                // udate number of bytes moved
182                done++;
183
184                // enable TX_IRQ
185                hal_remote_s32( XPTR( tty_cxy , base + TTY_TX_IRQ_ENABLE ) , 1 );
186            }
187            else                                // block & deschedule if TX_FIFO full
188            {
189                // block on ISR
190                thread_block( XPTR( local_cxy , CURRENT_THREAD ) , THREAD_BLOCKED_ISR );
191
192                // deschedule
193                sched_yield( "TTY_TX_FIFO full" ); 
194            }
195        }
196
197        // set error status in command and return
198        hal_remote_s32( error_xp , 0 );
199    }
200    ///////////////////////////
201    else if( type == TXT_READ )       // read several bytes from TTY_RX FIFO   
202    {
203        fifo = &tty_rx_fifo[channel];
204
205        done = 0;
206
207        while( done < count )
208        {
209            if( fifo->sts > 0 )               // get byte from FIFO if not empty
210            {
211                // get one byte from FIFO
212                char byte = fifo->data[fifo->ptr];
213
214#if DEBUG_HAL_TXT_RX
215uint32_t rx_cycle = (uint32_t)hal_get_cycles();
216if( DEBUG_HAL_TXT_RX < rx_cycle )
217printk("\n[%s] thread[%x,%x] get character <%c> from TXT%d_RX fifo / cycle %d\n",
218__FUNCTION__, this->process->pid, this->trdid, byte, channel, rx_cycle );
219#endif
220                // update FIFO state
221                fifo->ptr = (fifo->ptr + 1) % TTY_FIFO_DEPTH;
222                hal_atomic_add( &fifo->sts , -1 );
223
224                // set byte to command buffer
225                hal_remote_sb( buf_xp + done , byte );
226
227                // udate number of bytes
228                done++;
229            }
230            else                             //  deschedule if FIFO empty
231            {
232                // block on ISR
233                thread_block( XPTR( local_cxy , CURRENT_THREAD ) , THREAD_BLOCKED_ISR );
234   
235                // deschedule
236                sched_yield( "TTY_RX_FIFO empty" );
237            }
238        }  // end while
239
240        // set error status in command
241        hal_remote_s32( error_xp , 0 );
242    }
243    else
244    {
245        assert( false , "illegal TXT command\n" );
246    }
247
248#if (DEBUG_SYS_READ & 1)
249if( type == TXT_READ ) exit_tty_cmd_read = (uint32_t)hal_get_cycles();
250#endif
251
252#if (DEBUG_SYS_WRITE & 1)
253if( type == TXT_WRITE ) exit_tty_cmd_write = (uint32_t)hal_get_cycles();
254#endif
255
256}  // end soclib_tty_cmd()
257
258/////////////////////////////////////////////////////////////////
259void __attribute__ ((noinline)) soclib_tty_isr( chdev_t * chdev )
260{
261    thread_t   * server;            // pointer on TXT chdev server thread
262    lid_t        server_lid;        // local index of core running the server thread
263    uint32_t     channel;           // TXT chdev channel
264    bool_t       is_rx;             // TXT chdev direction
265    char         byte;              // byte value
266    xptr_t       owner_xp;          // extended pointer on TXT owner process
267    cxy_t        owner_cxy;         // TXT owner process cluster
268    process_t  * owner_ptr;         // local pointer on TXT owner process
269    pid_t        owner_pid;         // TXT owner process identifier
270    tty_fifo_t * fifo;              // pointer on TTY_TX or TTY_RX FIFO
271    cxy_t        tty_cxy;           // soclib_tty cluster
272    uint32_t   * tty_ptr;           // soclib_tty segment base address
273    uint32_t   * base;              // soclib_tty channel base address
274    xptr_t       status_xp;         // extended pointer on TTY_STATUS register
275    xptr_t       write_xp;          // extended pointer on TTY_WRITE register
276    xptr_t       read_xp;           // extended pointer on TTY_READ register
277    xptr_t       parent_xp;         // extended pointer on parent process
278    cxy_t        parent_cxy;        // parent process cluster
279    process_t  * parent_ptr;        // local pointer on parent process
280    thread_t   * parent_main_ptr;   // extended pointer on parent process main thread
281    xptr_t       parent_main_xp;    // local pointer on parent process main thread
282
283    // get TXT chdev channel, direction, server thread, and server core
284    channel    = chdev->channel;
285    is_rx      = chdev->is_rx;
286    server     = chdev->server;
287    server_lid = server->core->lid;
288
289#if (DEBUG_SYS_READ & 1)
290if( is_rx ) enter_tty_isr_read = (uint32_t)hal_get_cycles();
291#endif
292
293#if (DEBUG_SYS_WRITE & 1)
294if( is_rx == 0 ) enter_tty_isr_write = (uint32_t)hal_get_cycles();
295#endif
296
297#if DEBUG_HAL_TXT_RX
298uint32_t rx_cycle = (uint32_t)hal_get_cycles();
299#endif
300
301#if DEBUG_HAL_TXT_TX
302uint32_t tx_cycle = (uint32_t)hal_get_cycles();
303#endif
304
305    // get SOCLIB_TTY peripheral cluster and local pointer
306    tty_cxy = GET_CXY( chdev->base );
307    tty_ptr = GET_PTR( chdev->base );
308
309    // get channel base address
310    base    = tty_ptr + TTY_SPAN * channel;
311
312    // get extended pointer on TTY registers
313    status_xp = XPTR( tty_cxy , base + TTY_STATUS );
314    write_xp  = XPTR( tty_cxy , base + TTY_WRITE );
315    read_xp   = XPTR( tty_cxy , base + TTY_READ );
316
317    /////////////////////////// handle RX //////////////////////
318    if( is_rx )
319    {
320        fifo = &tty_rx_fifo[channel];
321
322        // try to move bytes until TTY_READ register empty
323        while( hal_remote_l32( status_xp ) & TTY_STATUS_RX_FULL )   
324        {
325            // get one byte from TTY_READ register & acknowledge RX_IRQ
326            byte = (char)hal_remote_lb( read_xp );
327
328            // filter special character ^Z  => block TXT owner process
329            if( byte == 0x1A ) 
330            {
331
332#if DEBUG_HAL_TXT_RX
333if( DEBUG_HAL_TXT_RX < rx_cycle )
334printk("\n[%s] read ^Z character from TXT%d\n", __FUNCTION__, channel );
335#endif
336                // get pointers on TXT owner process in owner cluster
337                owner_xp  = process_txt_get_owner( channel );
338               
339                // check process exist
340                assert( (owner_xp != XPTR_NULL) ,
341                "TXT owner process not found\n" );
342
343                // get relevant infos on TXT owner process
344                owner_cxy = GET_CXY( owner_xp );
345                owner_ptr = GET_PTR( owner_xp );
346                owner_pid = hal_remote_l32( XPTR( owner_cxy , &owner_ptr->pid ) );
347
348// TXT owner cannot be the INIT process
349assert( (owner_pid != 1) , "INIT process cannot be the TXT owner" );
350
351                // get parent process descriptor pointers
352                parent_xp  = hal_remote_l64( XPTR( owner_cxy , &owner_ptr->parent_xp ) );
353                parent_cxy = GET_CXY( parent_xp );
354                parent_ptr = GET_PTR( parent_xp );
355
356                // get pointers on the parent process main thread
357                parent_main_ptr = hal_remote_lpt(XPTR(parent_cxy,&parent_ptr->th_tbl[0])); 
358                parent_main_xp  = XPTR( parent_cxy , parent_main_ptr );
359
360                // transfer TXT ownership
361                process_txt_transfer_ownership( owner_xp );
362
363                // mark for block all threads in all clusters, but the main
364                process_sigaction( owner_pid , BLOCK_ALL_THREADS );
365
366                // block the main thread
367                xptr_t main_xp = XPTR( owner_cxy , &owner_ptr->th_tbl[0] );
368                thread_block( main_xp , THREAD_BLOCKED_GLOBAL );
369
370                // atomically update owner process termination state
371                hal_remote_atomic_or( XPTR( owner_cxy , &owner_ptr->term_state ) ,
372                                      PROCESS_TERM_STOP );
373
374                // unblock the parent process main thread
375                thread_unblock( parent_main_xp , THREAD_BLOCKED_WAIT );
376
377                return;
378            }
379
380            // filter special character ^C  => kill TXT owner process
381            if( byte == 0x03 )
382            {
383
384#if DEBUG_HAL_TXT_RX
385if( DEBUG_HAL_TXT_RX < rx_cycle )
386printk("\n[%s] read ^C character from TXT%d\n", __FUNCTION__, channel );
387#endif
388                // get pointer on TXT owner process in owner cluster
389                owner_xp  = process_txt_get_owner( channel );
390
391// check process exist
392assert( (owner_xp != XPTR_NULL) , "TXT owner process not found\n" );
393
394                // get relevant infos on TXT owner process
395                owner_cxy = GET_CXY( owner_xp );
396                owner_ptr = GET_PTR( owner_xp );
397                owner_pid = hal_remote_l32( XPTR( owner_cxy , &owner_ptr->pid ) );
398
399// TXT owner cannot be the INIT process
400assert( (owner_pid != 1) , "INIT process cannot be the TXT owner" );
401
402#if DEBUG_HAL_TXT_RX
403if( DEBUG_HAL_TXT_RX < rx_cycle )
404printk("\n[%s] TXT%d owner is process %x\n",
405__FUNCTION__, channel, owner_pid );
406#endif
407                // get parent process descriptor pointers
408                parent_xp  = hal_remote_l64( XPTR( owner_cxy , &owner_ptr->parent_xp ) );
409                parent_cxy = GET_CXY( parent_xp );
410                parent_ptr = GET_PTR( parent_xp );
411
412                // get pointers on the parent process main thread
413                parent_main_ptr = hal_remote_lpt(XPTR(parent_cxy,&parent_ptr->th_tbl[0])); 
414                parent_main_xp  = XPTR( parent_cxy , parent_main_ptr );
415
416                // transfer TXT ownership
417                process_txt_transfer_ownership( owner_xp );
418
419                // remove process from TXT list
420                process_txt_detach( owner_xp );
421
422                // close all open files
423                process_fd_clean_all( owner_xp );
424
425                // mark for delete all thread in all clusters, but the main
426                process_sigaction( owner_pid , DELETE_ALL_THREADS );
427               
428#if DEBUG_HAL_TXT_RX
429if( DEBUG_HAL_TXT_RX < rx_cycle )
430printk("\n[%s] marked for delete all threads of process but main\n",
431__FUNCTION__, owner_pid );
432#endif
433                // block main thread
434                xptr_t main_xp = XPTR( owner_cxy , &owner_ptr->th_tbl[0] );
435                thread_block( main_xp , THREAD_BLOCKED_GLOBAL );
436
437#if DEBUG_HAL_TXT_RX
438if( DEBUG_HAL_TXT_RX < rx_cycle )
439printk("\n[%s] blocked process %x main thread\n",
440__FUNCTION__, owner_pid );
441#endif
442
443                // atomically update owner process termination state
444                hal_remote_atomic_or( XPTR( owner_cxy , &owner_ptr->term_state ) ,
445                                      PROCESS_TERM_KILL );
446
447                // unblock the parent process main thread
448                thread_unblock( parent_main_xp , THREAD_BLOCKED_WAIT );
449
450#if DEBUG_HAL_TXT_RX
451if( DEBUG_HAL_TXT_RX < rx_cycle )
452printk("\n[%s] unblocked parent process %x main thread\n",
453__FUNCTION__, hal_remote_l32( XPTR( parent_cxy , &parent_ptr->pid) ) );
454#endif
455                return;
456            }
457
458            // write byte in TTY_RX FIFO if not full / discard byte if full
459            if ( fifo->sts < TTY_FIFO_DEPTH )
460            {
461
462#if DEBUG_HAL_TXT_RX
463if( DEBUG_HAL_TXT_RX < rx_cycle )
464printk("\n[%s] put character <%c> to TXT%d_RX fifo\n",
465__FUNCTION__, byte, channel );
466#endif
467                // store byte into FIFO
468                fifo->data[fifo->ptw] = (char)byte; 
469
470                // avoid race
471                hal_fence();
472
473                // update RX_FIFO state
474                fifo->ptw = (fifo->ptw + 1) % TTY_FIFO_DEPTH;
475                hal_atomic_add( &fifo->sts , 1 );
476
477                // unblock TXT_RX server thread
478                thread_unblock( XPTR( local_cxy , server ) , THREAD_BLOCKED_ISR );
479
480                // send IPI to core running server thread if required
481                if( server_lid != CURRENT_THREAD->core->lid )
482                {
483                    dev_pic_send_ipi( local_cxy , server_lid );
484                }
485            }
486            else
487            {
488                printk("\n[WARNING] %s : TTY_RX_FIFO[%d] full => discard character <%x>\n",
489                __FUNCTION__, channel, (uint32_t)byte );
490            }
491        }  // end while TTY_READ register full
492
493    }  // end RX
494
495    ///////////////////////  handle TX  /////////////////////////////
496    else
497    {
498        fifo = &tty_tx_fifo[channel];
499
500        // try to move bytes until TX_FIFO empty
501        while( fifo->sts > 0 )
502        {
503            // write one byte to TTY_WRITE register if empty / exit loop if full
504            if( (hal_remote_l32( status_xp ) & TTY_STATUS_TX_FULL) == 0 ) 
505            {
506                // get one byte from TX_FIFO
507                byte = fifo->data[fifo->ptr];
508
509#if DEBUG_HAL_TXT_TX
510if( DEBUG_HAL_TXT_TX < tx_cycle )
511printk("\n[%s] get character <%c> from TXT%d_TX fifo\n",
512__FUNCTION__, byte, channel );
513#endif
514                // update TX_FIFO state
515                fifo->ptr = (fifo->ptr + 1) % TTY_FIFO_DEPTH;
516                hal_atomic_add( &fifo->sts , -1 );
517
518                // write byte to TTY_WRITE register & acknowledge TX_IRQ
519                hal_remote_sb( write_xp , byte );
520            }
521        }
522
523        // disable TX_IRQ
524        hal_remote_s32( XPTR( tty_cxy , base + TTY_TX_IRQ_ENABLE ) , 0 );
525
526        // unblock TXT_TX server thread
527        thread_unblock( XPTR( local_cxy , server ) , THREAD_BLOCKED_ISR );
528
529        // send IPI to core running server thread if required
530        if( server_lid != CURRENT_THREAD->core->lid )
531        {
532            dev_pic_send_ipi( local_cxy , server_lid );
533        }
534
535    }  // end TX
536
537    hal_fence();
538
539#if (DEBUG_SYS_READ & 1)
540if( is_rx ) exit_tty_isr_read = (uint32_t)hal_get_cycles();
541#endif
542
543#if (DEBUG_SYS_WRITE & 1)
544if( is_rx == 0 ) exit_tty_isr_write = (uint32_t)hal_get_cycles();
545#endif
546
547}  // end soclib_tty_isr()
548
549/////////////////////////////////////////////////////////////
550void __attribute__ ((noinline)) soclib_tty_aux( void * args )
551{
552    uint32_t   status;
553    bool_t     empty;
554    uint32_t   i;
555
556    xptr_t       dev_xp = ((txt_sync_args_t *)args)->dev_xp;
557    const char * buffer = ((txt_sync_args_t *)args)->buffer;
558    uint32_t     count  = ((txt_sync_args_t *)args)->count;
559   
560    // get chdev cluster and local pointer
561    cxy_t     dev_cxy = GET_CXY( dev_xp );
562    chdev_t * dev_ptr = GET_PTR( dev_xp );
563
564    // get extended pointer on TTY channel base address
565    xptr_t tty_xp = (xptr_t)hal_remote_l64( XPTR( dev_cxy , &dev_ptr->base ) );
566
567    // get TTY channel segment cluster and local pointer
568    cxy_t      tty_cxy = GET_CXY( tty_xp );
569    uint32_t * tty_ptr = GET_PTR( tty_xp );
570
571    // get extended pointers on TTY_WRITE & TTY_STATUS registers
572    xptr_t write_xp  = XPTR( tty_cxy , tty_ptr + TTY_WRITE );
573    xptr_t status_xp = XPTR( tty_cxy , tty_ptr + TTY_STATUS );
574
575    // loop on characters
576    for( i = 0 ; i < count ; i++ )
577    {
578        // busy waiting policy on TTY_STATUS register
579        do
580        {
581            // get TTY_STATUS
582            status = hal_remote_l32( status_xp );
583            empty  = ( (status & TTY_STATUS_TX_FULL) == 0 );
584
585            // transfer one byte if TX buffer empty
586            if ( empty )  hal_remote_sb( write_xp , buffer[i] );
587        }
588        while ( empty == false );
589    }
590}  // end soclib_tty_aux()
591
592
593
Note: See TracBrowser for help on using the repository browser.