6.18.1.3 Customizing Helper Functions for XC16 and XC-DSC

The standard I/O relies on helper functions described in Syscall Interface. These functions include read(), write(), open(), and close() which are called to read, write, open or close handles that are associated with standard I/O FILE pointers. The sources for these libraries are provided for you to customize as you wish.

It is recommended that these customized functions be allocated in a named section that begins with .libc. This will cause the linker to allocate them near to the rest of the library functions, which is where they ought to be.

The simplest way to redirect standard I/O to the peripheral of your choice is to select one of the default handles already in use. Also, you could open files with a specific name via fopen() by rewriting open() to return a new handle to be recognized by read() or write() as appropriate.

If only a specific peripheral is required, then you could associate handle 1 == stdout or 2 == stderr to another peripheral by writing the correct code to talk to the interested peripheral.

A complete generic solution might be:

/* should be in a header file */
enum my_handles {
    handle_stdin,
    handle_stdout,
    handle_stderr,
    handle_can1,
    handle_can2,
    handle_spi1,
    handle_spi2,
};

int _ ___attribute_ ___((_ ___weak_ ___, _ ___section_ ___(".libc")))
  open(const char *name, int access, int mode) {
     switch (name[0]) {
       case 'i' : return handle_stdin;
       case 'o' : return handle_stdout;
       case 'e' : return handle_stderr;
       case 'c' : return handle_can1;
       case 'C' : return handle_can2;
       case 's' : return handle_spi1;
       case 'S' : return handle_spi2;
       default: return handle_stderr;
    }
}

Single letters were used in this example because they are faster to check and use less memory. However, if memory is not an issue, you could use strcmp to compare full names.

In write(), you would write:

int __attribute__((__section__(".libc.write")))
  write(int handle, void *buffer, unsigned int len) {
     int i;
     volatile UxMODEBITS *umode = &U1MODEbits;
     volatile UxSTABITS *ustatus = &U1STAbits;
     volatile unsigned int *txreg = &U1TXREG;
     volatile unsigned int *brg = &U1BRG;

     switch (handle)
     {
     default:
     case 0:
     case 1:
     case 2:
        if ((_ ___C30_UART != 1) && (&U2BRG)) {
           umode = &U2MODEbits;
           ustatus = &U2STAbits;
           txreg = &U2TXREG;
           brg = &U2BRG;
        }
        if ((umode->UARTEN) == 0)
        {
           *brg = 0;
           umode->UARTEN = 1;
        }
        if ((ustatus->UTXEN) == 0)
        {
           ustatus->UTXEN = 1;
        }
        for (i = len; i; --i)
        {
           while ((ustatus->TRMT) ==0);
           *txreg = *(char*)buffer++;
        }
        break;
     case handle_can1:  /* code to support can1 */
        break;
     case handle_can2:  /* code to support can2 */
        break;
     case handle_spi1:  /* code to support spi1 */
        break;
     case handle_spi2:  /* code to support spi2 */
        break;
     }
     return(len);
}

where you would fill in the appropriate code as specified in the comments.

Now you can use the generic C stdio I/O features to write to another port:

FILE *can1 = fopen("c","w");
     fprintf(can1,"This will be output through the can\n");