Jump to content
IGNORED

libti99 RS232 code needs some slight debugging.


tschak909

Recommended Posts

Hey guys,

 

As some of you know, I've been writing a PLATO terminal emulator and have ported it over to the TI-99/4A, thus far, it has successfully been running on TIPI enabled machines, with excellent results.

 

It also works under the TI994W emulator, with RS232/1 emulation.

 

HOWEVER,

 

any form of real RS232 hardware, be it the PHP1700 sidecar, or the NanoPEBv1, the terminal refuses to receive or transmit any data.

 

I'm thinking the port may be initialized incorrectly, but I am not sure...

 

So I'm splaying out the source code for the routines I am using from Tursi's libti99, and maybe there may be something that was missed.

 

The relevant code in PLATOTERM is in io.c:

#include "io.h"
#include "protocol.h"
#include "vdp.h"
#include "conio.h"
#ifdef RS232
#include "rs232.h"
#endif
#ifdef TIPI
#include "tipi_msg.h"
#include "ti_socket.h"
#endif

BaudRate io_baud_rate;

void io_init(void)
{
#ifdef RS232
  rs232_setcontrol(RS232_CARD,RS232_9902A,RS232_CTRL_STOP_1|RS232_CTRL_NO_PARITY|RS232_CTRL_8_BIT);
  rs232_setbps(RS232_CARD,RS232_9902A,RS232_BPS_1200);
  io_baud_rate=BAUD_1200;
#endif
}

void io_set_baud_rate(void)
{
  #ifdef RS232
  int new_baud_rate;
  switch(io_baud_rate)
    {
    case BAUD_300:
      new_baud_rate=RS232_BPS_300;
      break;
    case BAUD_1200:
      new_baud_rate=RS232_BPS_1200;
      break;
    case BAUD_2400:
      new_baud_rate=RS232_BPS_2400;
      break;
    case BAUD_4800:
      new_baud_rate=RS232_BPS_4800;
      break;
    case BAUD_9600:
      new_baud_rate=RS232_BPS_9600;
      break;
    case BAUD_19200:
      new_baud_rate=RS232_BPS_19200;
      break;
    case BAUD_38400:
      new_baud_rate=RS232_BPS_38400;
      break;
    }
  rs232_setbps(RS232_CARD,RS232_9902A,new_baud_rate);
#endif
}

void io_toggle_baud_rate(void)
{
#ifdef RS232
  if (io_baud_rate==BAUD_38400)
    io_baud_rate=BAUD_300;
  else
    io_baud_rate++;
  io_set_baud_rate();
#endif
}

void io_send_byte(int b)
{
#ifdef RS232
  rs232_writebyte(RS232_CARD,RS232_9902A,b);
#endif
}

void io_main(void)
{
#ifdef RS232
  int ib;
  unsigned char b;
  if (rs232_poll(RS232_CARD,RS232_9902A))
    {
      ib=rs232_readbyte(RS232_CARD,RS232_9902A);
      b=ib;
      ShowPLATO(&b,1);
    }
#endif
}

void io_done()
{
}

and pulling down into the rs232 code, each of these functions, is a C wrapper which follows the same pattern:

* Activate card

* do its thing

* Deactivate card

 

rs232_setcontrol.c

// RS232 code for the TI-99/4A by Tursi
// You can copy this file and use it at will 

#include "rs232.h"

// sets the specified line format, use the RS232_CTRL_xxx bits
void rs232_setcontrol(int card, int uart, int control) {
    int rawCRU = rs232raw_getuart(card, uart);

    __asm__ (
        "  mov %0,r12\n"        // get the rawcru address
        "  sbo 31\n"            // reset
        "  sbz 21\n"            // rts/cts interrupts off
        "  sbz 20\n"            // timer ints off
        "  sbz 19\n"            // tx int off
        "  sbz 18\n"            // rx int off
        "  sbz 17\n"            // clear abort/BREAK
        "  sbz 15\n"            // clear loopback test mode
        "  sbo 14\n"            // request to write control word
        "  swpb %1\n"           // get the byte into the msb
        "  ldcr %1,8\n"         // write it
        "  swpb %1\n"           // fix the reg in case gcc wants it

        : : "r"(rawCRU), "r"(control) : "r12"
    );

    rs232raw_deactivateCard(card);
}

rs232_setbps.c

// RS232 code for the TI-99/4A by Tursi
// You can copy this file and use it at will 

#include "rs232.h"

// set the bitrate on the card and uart (sets both directions the same)
void rs232_setbps(int card, int uart, int bps) {
    int rawCRU = rs232raw_getuart(card, uart);

    __asm__ (
        "  mov %0,r12\n"        // get the rawcru address
        "  sbo 12\n"            // request write to recv rate
        "  ldcr %1,11\n"        // load recv rate (auto-decrements to send rate)
        "  ldcr %1,11\n"        // load send rate (Nouspikel says you can do both at once, but safer this way)
        : : "r" (rawCRU), "r" (bps) : "r12"
    );

    rs232raw_deactivateCard(card);
}

rs232_writebyte.c

// RS232 code for the TI-99/4A by Tursi
// You can copy this file and use it at will 

#include "rs232.h"

// writes a byte to the specified serial port - returns 0 if the byte could not be written
// (because the write register is full)
int rs232_writebyte(int card, int uart, int ch) {
    int rawCRU = rs232raw_getuart(card, uart);
    if (rs232raw_checkstatus(rawCRU) & 0x02) {
        rs232raw_writebyte(rawCRU, ch);
        rs232raw_deactivateCard(card);
        return 1;
    } else {
        rs232raw_deactivateCard(card);
        return 0;
    }
}

rs232_readbyte.c

// RS232 code for the TI-99/4A by Tursi
// You can copy this file and use it at will 

#include "rs232.h"

// reads a byte from the specified serial port, blocks - so always check rs232_poll first!
int rs232_readbyte(int card, int uart) {
    int ret;
    int rawCRU = rs232raw_getuart(card, uart);

    // infinite loop, so poll before you call me!
    while (!rs232raw_poll(rawCRU)) { }
    ret = rs232raw_readbyte(rawCRU);

    rs232raw_deactivateCard(card);

    return ret;
}

rs232_poll.c

// RS232 code for the TI-99/4A by Tursi
// You can copy this file and use it at will 

#include "rs232.h"

// test if a byte is available at the specied serial port (returns 0 if not, other value if so)
int rs232_poll(int card, int uart) {
    int ret;
    int rawCRU = rs232raw_getuart(card, uart);
    ret = rs232raw_poll(rawCRU);
    rs232raw_deactivateCard(card);

    return ret;
}

Now, each of these, calls various raw functions, including rs232raw_getuart() which is, supposed to activate the card.

// RS232 code for the TI-99/4A by Tursi
// You can copy this file and use it at will 

#include "rs232.h"

// activates the card, and calculates and returns the correct uart rawCRU address for
// the rest of the raw functions. You MUST call deactivateCard when done!
int rs232raw_getuart(int card, int uart) {
    rs232raw_activateCard(card);
    return card+uart;
}

rs232raw_activateCard.c

// RS232 code for the TI-99/4A by Tursi
// You can copy this file and use it at will 

#include "rs232.h"

// turns off the card and the card LED
void rs232raw_activateCard(int card) {
    __asm__ (
        "mov %0,r12\n\tsbo 0\n\rsbo 7" : : "r"(card) : "r12"
    );
}

and the corresponding rs232raw_deactivateCard.c

// RS232 code for the TI-99/4A by Tursi
// You can copy this file and use it at will 

#include "rs232.h"

// turns off the card and the card LED
void rs232raw_deactivateCard(int card) {
    __asm__ (
        "mov %0,r12\n\tsbz 7\n\rsbz 0" : : "r" (card) : "r12"
    );
}

rs232raw_poll.c

// RS232 code for the TI-99/4A by Tursi
// You can copy this file and use it at will 

#include "rs232.h"

// test if a byte is available at the specied serial port (returns 0 if not, other value for true)
int rs232raw_poll(int rawCRU) {
    int ret;

    __asm__ (
        "MOV %1,R12\n\tCLR %0\n\tTB 21\n\tJNE NCH\n\tSETO %0\nNCH" : "=rm" (ret) : "r" (rawCRU) : "r12"
    );

    return ret;
}

rs232raw_geterrs.c

// RS232 code for the TI-99/4A by Tursi
// You can copy this file and use it at will 

#include "rs232.h"

// return the error bits from the specified port (we don't check the timer here)
// 0x01 = reception error (set for any of the other three)
// 0x02 = parity error
// 0x04 = overflow error (receive data lost)
// 0x08 = frame error (line corruption or incorrect line format)
int rs232raw_geterrs(int rawCRU) {
    int ret;

    __asm__ (
        "mov %1,r12\n\tai r12,18\n\tclr %0\n\tstcr %0,4\n\tswpb %0" : "=rm" (ret) : "r" (rawCRU) : "r12"
    );

    return ret;
}

rs232raw_checkstatus.c

// RS232 code for the TI-99/4A by Tursi
// You can copy this file and use it at will 

#include "rs232.h"

// check the TX and RX status
// 0x01 = receive buffer contains a byte
// 0x02 = transmit buffer empty
// 0x04 = transmission line clear
// 0x08 = timer error (not used in this code)
// 0x10 = time elapsed (not used in this code)
// 0x20 = RTS (inverted)
// 0x40 = DSR (inverted)
// 0x80 = CTS (inverted)
int rs232raw_checkstatus(int rawCRU) {
    int ret;

    __asm__ (
        "mov %1,r12\n\tai r12,42\n\tclr %0\n\tstcr %0,8\n\tswpb %0" : "=rm" (ret) : "r" (rawCRU) : "r12"
    );

    return ret;
}

rs232raw_readbyte.c

// RS232 code for the TI-99/4A by Tursi
// You can copy this file and use it at will 

#include "rs232.h"

// reads a byte from the specified serial port - whatever is there. Does not check or
// block, so if you need correct data do a poll first!
int rs232raw_readbyte(int rawCRU) {
    int ret;

    __asm__ (
        "  mov %1,r12\n"        // get rawCRU
        "  clr %0\n"            // clear rx register
        "  stcr %0,8\n"         // get byte
        "  swpb %0\n"           // make LSB
        "  sbz 18\n"            // reset rx flag
        : "=rm" (ret) : "r" (rawCRU) : "r12"
    );

    return ret;
}

rs232raw_writebyte.c

// RS232 code for the TI-99/4A by Tursi
// You can copy this file and use it at will 

#include "rs232.h"

// writes a byte to the specified serial port - does not check the transmission register!
// this does manipulate RTS/DCD since everyone does 
void rs232raw_writebyte(int rawCRU, int ch) {
    __asm__ (
        "  mov %0,r12\n"        // get rawCRU
        "  sbo 16\n"            // set RTS low (this is what the card and FlipSide did...? maybe enables the write?)
        "  swpb %1\n"           // get the value into the MSB
        "  ldcr %1,8\n"         // write the byte
        "  sbz 16\n"            // set RTS high (will happen after transmission)
        /* "  swpb %1\n"                // fix the reg in case gcc wants it */
        : : "r" (rawCRU), "r" (ch) : "r12"
    );
}

....sorry for the huge pastes, but I'm really scratching my head here, I'm not sure what's wrong, as I'm not familiar with the behavior of the underlying UART or I/O subsystem enough to be able to diagnose.. can anyone provide some insight?

 

Thanks in advance,

-Thom

Link to comment
Share on other sites

Could it be your RS232 cable? Does it work with other software? (like TI BASIC)

 

Here is one source of pinout info: http://www.nouspikel.com/ti99/titechpages.htm

I tried it on my set up which works fine with fast term and no data seems to be sent out

 

Sent from my LG-V530 using Tapatalk

Link to comment
Share on other sites

Wrong section or not, let me copy out what I sent to Thom earlier tonight... I rebuilt my PEB and sat down to look at it. I confirmed that the configuration was wrong and poked around my docs until I found the original document I wrote my BBS from 28 years ago (Genial!)

 

The code in that document relied on a precise sequence of initialization, which I had of course removed when I did the library, so basically nothing went to the right place. I updated the library to set the configuration bits directly instead of making assumptions, and it seems to work now. Since it worked in TI99W, I suspect that TI99W either doesn't set all four register selection bits on reset or it clears them all after a register write (but that's a guess).

  • Like 3
Link to comment
Share on other sites

In the for what it's worth department, I tried writing, in assembler, a polled routine that could enqueue characters.

 

It could be my coding, but I could not reliably catch every character when I sent at 9600 bps. ( still can't believe this is true so more testing required)

 

Contrast this with the result that interrupt driven receive seems to run at 38.4K bps reliably.

 

It could be we need an interrupt driven system for a reliable terminal emulator on this old girl.

Link to comment
Share on other sites

That seems odd to me... just the ROM based interrupt handler that tries to figure out which DSR to service for interrupt should take longer than a poll+service sequence...? Is your system modified for quicker interrupt response?

 

Odd to me too. I was hoping to make a way to avoid interrupts for rapid downloads.

 

No I have a stock console. The only new thing is a SAMS card.

I don't understand it either so let's suspect my code until further notice.

 

But perhaps someone could cobble together a dedicated polled loop in C that writes to a buffer to see how wrong I am.

 

Added my code:

I originally used indirect addressing to write the character, and switched to index addressing and a manual increment when my results seemed sketchy.

HEX 3000 CONSTANT BUFF    \ test buffer at >3000
DECIMAL
CODE STRAIGHT ( len -- len' )
        R12 RPUSH,            \ save R12
        PORT @@ R12 MOV,      \ select the 9902
       *SP+ R3 MOV,           \ buffer -> R3
        R1 CLR,               \ reset char counter
        BEGIN,
           TOS R3 CMP,
        NE WHILE,
              BEGIN,
                 21 TB,
              EQ UNTIL,      \ wait 1st character
              BUFF R3 () 8 STCR,  \ store the character indexed addressing
              18 SBO,        \ clr rcv buffer
              R3 INC,        \ bump index
        REPEAT,
        R12 RPOP,
        R3 TOS MOV,             \ actual char count recv'd
        NEXT,
        ENDCODE

Edited by TheBF
Link to comment
Share on other sites

Well, I _did_ do that, I wrote a macro:

#define IO_POLL while (rs232_poll(RS232_CARD,RS232_9902A)) io_buffer[io_buflen++]=rs232_readbyte(RS232_CARD,RS232_9902A)

and I sprinkled that ALL OVER THE CODE. It slowed things down a lot, and didn't really catch very much more than when I did it in one place.

 

-Thom

Edited by tschak909
  • Like 1
Link to comment
Share on other sites

In the for what it's worth department, I tried writing, in assembler, a polled routine that could enqueue characters.

 

It could be my coding, but I could not reliably catch every character when I sent at 9600 bps. ( still can't believe this is true so more testing required)

Were the 9902 interrupts disabled and how much were you trying to do within the loop besides just testing for a received character?

 

19.2K is achievable in a polling loop, as is 38.4K.

Link to comment
Share on other sites

So that everyone understands, PLATOTERM is _NOT_ a text terminal. It does not function to the rules of a text terminal.

 

This is the text plotting routine, which plots one dot at a time:

https://github.com/tschak909/platoterm99/blob/master/src/screen.c#L144

/**
 * screen_char_draw(Coord, ch, count) - Output buffer from ch* of length count as PLATO characters
 */
void screen_char_draw(padPt* Coord, unsigned char* ch, unsigned char count)
{
  short offset; /* due to negative offsets */
  int x;      /* Current X and Y coordinates */
  int y;
  int* px;   /* Pointers to X and Y coordinates used for actual plotting */
  int* py;
  unsigned char i; /* current character counter */
  unsigned char a; /* current character byte */
  unsigned char j,k; /* loop counters */
  char b; /* current character row bit signed */
  unsigned char width=FONT_SIZE_X;
  unsigned char height=FONT_SIZE_Y;
  unsigned short deltaX=1;
  unsigned short deltaY=1;
  unsigned char mainColor=current_foreground;
  unsigned char altColor=current_background;
  unsigned char *p;
  unsigned char* curfont;
  
  switch(CurMem)
    {
    case M0:
      curfont=font;
      offset=-32;
      break;
    case M1:
      curfont=font;
      offset=64;
      break;
    case M2:
      curfont=fontm23;
      offset=-32;
      break;
    case M3:
      curfont=fontm23;
      offset=32;      
      break;
    }

  if (CurMode==ModeRewrite)
    {
      altColor=current_background;
    }
  else if (CurMode==ModeInverse)
    {
      altColor=current_foreground;
    }
  
  if (CurMode==ModeErase || CurMode==ModeInverse)
    mainColor=current_background;
  else
    mainColor=current_foreground;

  bm_setforeground(mainColor);
  
  x=scalex((Coord->x&0x1FF));

  if (ModeBold)
    y=scaley((Coord->y+30)&0x1FF);
  else
    y=scaley((Coord->y+15)&0x1FF);

  if (FastText==padF)
    {
      goto chardraw_with_fries;
    }

  /* the diet chardraw routine - fast text output. */
  
  for (i=0;i<count;++i)
    {
      a=*ch;
      ++ch;
      a+=offset;
      p=&curfont[FONTPTR(a)];
      
      for (j=0;j<FONT_SIZE_Y;++j)
  	{
  	  b=*p;
	  
  	  for (k=0;k<FONT_SIZE_X;++k)
  	    {
  	      if (b<0) /* check sign bit. */
		{
		  bm_setforeground(mainColor);
		  if (CurMode==ModeErase)
		    bm_clearpixel(x,y);
		  else
		    bm_setpixel(x,y);
		}

	      ++x;
  	      b<<=1;
  	    }

	  ++y;
	  if (y>191)
	    y-=192;
	  x-=width;
	  /* if (x<0) */
	  /*   x=256+x; */
	  ++p;
  	}

      x+=width;
      /* if (x>255) */
      /* 	x=256-x; */
      y-=height;
      if (y<0)
	y=192+y;
    }

  return;

 chardraw_with_fries:
  if (Rotate)
    {
      deltaX=-abs(deltaX);
      width=-abs(width);
      px=&y;
      py=&x;
    }
    else
    {
      px=&x;
      py=&y;
    }
  
  if (ModeBold)
    {
      deltaX = deltaY = 2;
      width<<=1;
      height<<=1;
    }
  
  for (i=0;i<count;++i)
    {
      a=*ch;
      ++ch;
      a+=offset;
      p=&curfont[FONTPTR(a)];
      for (j=0;j<FONT_SIZE_Y;++j)
  	{
  	  b=*p;

	  if (Rotate)
	    {
	      px=&y;
	      py=&x;
	    }
	  else
	    {
	      px=&x;
	      py=&y;
	    }

	  // special 9918 specific hack for inverse video
	  if (CurMode==ModeInverse)
	    {
	      for (k=0;k<FONT_SIZE_X;++k)
	  	{
	  	  if (ModeBold)
	  	    {
	  	      bm_setpixel(*px+1,*py);
	  	      bm_setpixel(*px,*py+1);
	  	      bm_setpixel(*px+1,*py+1);
	  	    }
	  	  bm_setpixel(*px,*py);
	  	}
	    }
	  
  	  for (k=0;k<FONT_SIZE_X;++k)
  	    {
  	      if (b<0) /* check sign bit. */
		{
		  if (ModeBold)
		    {
		      bm_setpixel(*px+1,*py);
		      bm_setpixel(*px,*py+1);
		      bm_setpixel(*px+1,*py+1);
		    }
		  bm_setpixel(*px,*py);
		}
	      else
		{
		  if (CurMode==ModeInverse || CurMode==ModeRewrite)
		    {
		      if (ModeBold)
			{
			  bm_clearpixel(*px+1,*py);
			  bm_clearpixel(*px,*py+1);
			  bm_clearpixel(*px+1,*py+1);
			}
		      bm_clearpixel(*px,*py); 
		    }
		}

	      x += deltaX;
	      /* if (x>255) */
	      /* 	x=256-x; */
  	      b<<=1;
  	    }

	  y+=deltaY;
	  if (y>191)
	    y-=192;
	  x-=width;
	  /* if (x<0) */
	  /*   x=256+x; */
	  ++p;
  	}

      Coord->x+=width;
      /* if (Coord->x>255) */
      /* 	x=256-x; */
      x+=width;
      /* if (x>255) */
      /* 	x=256-x; */
      y-=height;
      if (y<0)
	y=192+y;
    }

  return;  
}

  • Like 1
Link to comment
Share on other sites

I don't think anyone here is trying to shoe-horn platoterm into the terminal emulator model.

 

You have two realistic options for serial data capture: interrupt or polling.

 

My impression is that the quick interrupt and the dsr-based interrupt routines discussed elsewhere aren't (easily?) usable in the c environment. That leaves you with polling, which in assembly works well at high speeds to read incoming, packeted data. Didn't you say you could control the data to some extent in another thread? If so, then the next step is to figure out how to code a higher-speed serial input routine. Maybe it requires an in-line assembly function to poll for the expected data.

 

Fred's Disk Manager 2000 is written in c and assembly. Maybe he has some ideas on how to get beyond this in the c environment, as his nano version does send and receive data at 38.4k. Presumably via a polling loop.

  • Like 3
Link to comment
Share on other sites

C isn't really much higher level than assembly, really. We aren't providing a framework, just some subroutines.

 

I think the problem in PlatoTerm boils down to how long it takes to complete a task to the screen. For instance - the text draw function above - that draws an entire string to the display, one pixel at a time. The CPU in the TI is not especially fast and accessing VDP, particularly if it needs to set the address every pixel, is fairly slow. At 1200bps you get about 8ms before you lose a character (if they are coming in continuously). On the TI CPU, that's roughly 1500 instructions (avg 12 cycles plus 4 cycles wait state). If we consider a best case for this call:

 

bm_setpixel(*px+1,*py);

 

That's going to break down to something like this:

 

- increment px (1)

- dereference px+1 and py (2)

- branch to bm_setpixel (1)

- calculate the bitmap address (5)

- set the VDP address (2)

- read the existing byte (1)

- calculate and merge the new pixel (2)

- reset the VDP address (2)

- calculate the color address (erf, this should be just an add, I'll fix that... 1)

- set VDP address (2)

- write color byte (1)

- return to caller (1)

... totals 21 instructions (and the reality is going to be slightly worse due to stack management and basic compiler imperfection). But assuming 21 instructions, that means you can draw about 71 pixels before you've lost a character (and that's if pixels are all you're doing - you also have to calculate which pixels to draw and where).

 

I would expect that adding IO_POLL inside these functions - if you didn't already try that - would help. You need it inside your VDP loops. But then there's the performance hit...

 

That can be helped a little bit... since you aren't doing disk (for now), you can bypass a lot of the extra work my RS232 functions do and leave the RS232 card on between calls. This saves all the math and the repeated CRU to turn it on and then back off again. It does mean you can't do any DSRLNK without explicitly turning it off, but we can deal with that later, if that day comes.

 

To that end, you can do something like this:

 

// somewhere globally visible...
int myRS232card;

...

// called from your initialization code, before the first time you need it...
// this maps in the card's DSR and gets the address to talk to it at
myRS232card = rs232raw_getuart(RS232_CARD,RS232_9902A);

...

// faster replacement for IO_POLL
// make sure io_buflen is an int, or you will be slowing yourself down unnecessarily
#define IO_POLL while (rs232raw_poll(myRS232card)) io_buffer[io_buflen++]=rs232raw_readbyte(myRS232card)

...

// if you ever need to turn off the card
// You can turn it back on with either rs232raw_activateCard(myRS232card) or just repeat the rs232raw_getuart,
// no significant different if you don't do it often.
rs232raw_deactivateCard(myRS232card);
Again, I don't have any experience with the RS232 interrupt - my main concern would be is it safe to have it on while doing VDP work? If it is, then it might be worth adding the necessary code for a little buffer handler. :) But the above will make the polling loop a lot faster, at least, so it might not slow things down as much.

 

The alternative, of course, may be to just admit defeat and say that with a true RS232, your bitrate is limited. While the massive speeds people are posting here are possible, popular clients from back in the day like Telco would sometimes drop characters above even 2400 bps, and that WAS just text.

 

I was curious, though, because I haven't had a chance to fire it up yet myself. But, does IRATA send you characters spontaneously, or only in response to a user action? Because if it's only in response to user input, you might consider a pre-buffering solution -- when you start to receive characters, drop what you're doing and do nothing but receive characters until you stop getting them for half a second or so. Then start processing from the buffer (rather than trying to draw and receive at the same time)? You could just spin a little sprite or something while receiving so the user knows it's not dead, once it's working.

Edited by Tursi
  • Like 3
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...