Dec 112017
Turbo C startup source to write PROMable C programs in Borland Turbo C version 2.0. | |||
---|---|---|---|
File Name | File Size | Zip Size | Zip Type |
BINTOHEX.C | 1367 | 581 | deflated |
C0ROMX.ASM | 2126 | 951 | deflated |
INSJUMP.C | 2254 | 877 | deflated |
ROMEXMPL.C | 982 | 460 | deflated |
ROMEXMPL.MAK | 345 | 164 | deflated |
ROMIZE.C | 8218 | 2437 | deflated |
TCROM.DOC | 14459 | 5841 | deflated |
Download File TCROM2.ZIP Here
Contents of the TCROM.DOC file
WRITING ROMABLE CODE IN TURBO C:
Updated 2/2/89 for Turbo C 2.0 by the author.
I recently completed a consulting project to design a
special-purpose controller using an embedded 8088 with firmware
in ROM. To create the firmware, I developed a scheme for
generating ROMable code from Turbo C.
The scheme utilizes the following:
1. Rules for writing C code.
2. ROMIZE.C, a program to post-process the Turbo C Compiler
assembler output.
3. INSJUMP.C, a program to patch the ROM binary code to
insert a restart jump instruction.
4. BINTOHEX.C, a program to produce Intel-format files for
downloading ROM images to an EPROM programmer.
5. C0ROMX.ASM, a replacement for C0.ASM, the startup code.
In addition, the scheme requires the Turboc C command-line
compiler, TCC.EXE; an assembler, Microsoft Version 3.0 or later,
or equivalent (I use Microsoft V5.0); TLINK or MS-Link; and
EXE2BIN (more about EXE2BIN later).
The scheme involves the following steps:
1. C code is written following a few rules.
2. TCC is used to translate the C code to assembly.
3. ROMIZE is used to perform certain modifications to the
assembly code.
4. The modified assembly code is assembled into a .OBJ file.
5. The .OBJ file is linked with the C0ROMX.OBJ startup code
and with other .OBJ files (if needed; these may be from C code
that has gone through the four above steps, or routines
originally coded in assembly). This produces a .EXE file.
6. EXE2BIN converts the .EXE into a binary ROM image.
7. INSJUMP inserts the reset jump into the ROM image.
8. If necessary, the binary ROM image is converted into
whatever format the particular EPROM programmer requires. (Mine
is a plug-in card for the PC which takes binary format directly,
so I don't have to do this step.)
All these steps can be run in a .BAT or MAKE file, making the
conversion of the original C code to ROM code almost automatic.
RULES AND ASSUMPTIONS:
The rules and assumptions for writing ROMable code using this
scheme are as follows:
1. The Compact memory model (small code, large data) is used.
This results in a maximum of 64K of code and initialized static
variables (i.e. constants) but up to 1 MB of Data space.
2. Initialized static variables are considered constants, and
may not be changed during program execution because they are
stored in ROM.
3. Variables defined as "extern" are considered constants.
Such variables would be used for font tables, for example. True
"variables" (which change during program execution) may not be
defined in one C module and declared as "extern" in another. The
only means of inter-module variable passing is through function
parameters and return values (a good programming practice
anyway). IT IS OK to define variables in a C module and reference
these as EXTRN's in assembler-coded modules, however.
4. Most of the Runtime Library routines may NOT be used,
because they have been compiled with the wrong segment groupings
for the ROM environment. Many of the library routines are not
usable, anyway, because they pertain to the DOS environment and
use system facilities that are not available in the ROM
environment. Library routines that are needed will have to be
recreated, either in C or assembly. You may want to order the
Turbo C Runtime Library from Borland as a starting point. I
ordered the Runtime Library several months into the project that
stimulated the creation of this system, but I didn't really need
it.
I was able to compile and debug over 90% of my code (which
was about 3,500 lines of C) using the Integrated Environment on a
PC, then re-compile (with #DEFINES and #IFDEFs to accommodate the
environmental differences) for ROM with little further debugging.
SEGMENT USAGE IN TURBO C:
Turbo C assigns uninitialized static variables to a segment
called _BSS and initialized static variables (i.e., constants) to
a segment called _DATA. These two segments are associated via a
GROUP directive, and assumed, under normal circumstances, to be
jointly addressable by the DS register. Instructions are
generated in the _TEXT segment, and assumed addressable by the CS
register.
In the ROM environment, the constants in the _DATA segment
and the code in the _TEXT segment are both located in the ROM and
addressed by CS. Only BSS is addressed by DS. Therefore to
convert the code generated by the compiler, ROMIZE is used as
follows:
WHAT ROMIZE DOES:
ROMIZE makes two passes through the assembly source code
generated by the command line compiler. In the first pass, it
reads the code and accumulates a table of all symbols defined in
the _DATA segment. There are only a few such symbols, since
Turbo C places all string constants in a single symbol "s@", and
refers to individual strings as "s@+n". Likewise, all
non-string constants are part of a single symbol called "d@" and
referred to by their displacements. There are however, a large
number of references to these symbols in the code.
In its second pass, ROMIZE re-reads the source code and
writes it out with modifications. The modifications are:
1. The command that groups _BSS and DATA together into DGROUP
is changed so that only _BSS is in DGROUP.
2. A new GROUP command is inserted creating CGROUP as _TEXT
and _DATA.
3. The ASSUME statement is changed from
ASSUME cs:_TEXT,ds:DGROUP
to:
ASSUME cs:CGROUP,ds:DGROUP
4. In any line of code with a reference to a symbol in the
_DATA segment, the group reference is changed from DGROUP to
CGROUP, or a CGROUP reference is added if no group name is
present.
5. Preceding a line of code with a reference to a symbol in
the _DATA segment may be a "mov x,ds" or a "push ds"instruction.
This is changed to a "mov x,cs" or "push cs".
6. Following a line of code with a reference to a symbol in
the _DATA segment may be a "push ds" instruction. This is changed
to a "push cs".
In order to do the looking backward and forward, ROMIZE
maintains a three-line pipeline, reading new lines into the head
of the pipe and writing lines from the end.
The various segment naming options of the compiler do not
produce the desired changes for the ROM environment. I would be
happy to pursue this subject further with anyone who is curious
about it.
LINKING:
You can use either TLINK or MS-LINK. The command line will,
of course be different for each. The "Warning - no stack segment"
will be produced by either. This is normal.
You must read the link map carefully to verify that your code
fit in the size of ROM you use. Consider the following partial
link map:
LINK : warning L4021: no stack segment
Start Stop Length Name Class
00000H 05A74H 05A75H _TEXT CODE
05A80H 07DF7H 02378H _DATA DATA
07DF8H 0A530H 02739H _BSS BSS
0A531H 0A531H 00000H _BSSEND BSSEND
Origin Group
0000:0 CGROUP
07DF:0 DGROUP
The .bin file that came out of EXE2BIN for the module that
produced this link map was A531 (hex) bytes long, however, this
module does fit in 32K. The key indicator is the sum of the
lengths of _TEXT and _DATA, and consequently, the starting
address of DGROUP (_BSS). Here it is 7DF8, about 500 bytes
under 32K.
MORE ABOUT EXE2BIN:
EXE2BIN is needed to convert the .EXE file into a binary
image that can be transmitted to an EPROM programmer. It may need
to perform "segment fixups" (segment relocation), if your program
includes constructs like the following:
static char *menu[] = {"Preview ","Add Before","Add After "};
This is a convenient way to generate arrays of strings for
such things as menus. This generates an array of pointers whose
segment parts must be relocated by EXE2BIN to the segment address
of the ROM. Therefore, when the EXE2BIN step is run, you will be
prompted to enter a segment address (F800 hex in the attached
example).
For those users of versions of DOS (such as 3.3) that do not
include EXE2BIN, there are several alternatives: Use EXE2BIN from
DOS 2.0 (I think 2.1 will work also), or use EXE2BIN from DOS 3.0
or 3.1 and patch it so it does not test for DOS version. The test
is at the beginning of these programs and is easily defeated.
Except for the version check, I know of no DOS version
dependencies in these programs. (I did all my development under
DOS 3.2, however, which still includes EXE2BIN).
The EXE2COM program available on CompuServe and many bulletin
boards may work in place of EXE2BIN (I have not tested it),
EXCEPT when segment relocation is required (i.e., you use the
above-described construct). It does not do segment relocation.
The fact that EXE2BIN stops and prompts you for a segment
relocation factor is an annoyance. You can use the PC Magazine
KEY-FAKE.COM (see PCMagnet) to supply the required value in a
batch or MAKE file. If KEY-FAKE is used in a MAKE file, you must
have invoked KEY-FAKE at least once (without arguments is fine)
before running MAKE. KEY-FAKE installs as a TSR the first time
it is invoked, and apparently, Turbo MAKE doesn't like programs
that do this, so if you invoke KEY-FAKE in a MAKE file
without it already being resident Turbo MAKE terminates with an
error 768.
MORE HINTS:
If you declare an array of strings as mentioned in the above
discussion, the "static" storage class is not necessary if the
array is defined outside of any function. If such an array is
defined as local to (within) a function, then be certain to add
"static", otherwise, Turbo C generates code to copy the array
into the function's local variable space (the stack). This is
unnecessary if the array is indeed a constant.
Although you won't be using many (any) library functions, you
may still want to include a number of standard header files.
The library functions you recreate ought to use the same
definitions as the library versions, anyway.
"dos.h" is an especially useful item. Although chances are
you won't be using any of the file I/O functions defined in it,
it includes some macros and in-line functions that are EXTREMELY
useful in the ROM environment such as MK_FP(), inport(),
outport(), FP_SEG(), FP_OFF(), peek() and poke().
FLOATING POINT LIMITATIONS:
If you need to use floating point arithmetic, you may have a
problem. If your target hardware includes a numeric co-processor
then you will have to order the Runtime Library and modify the
8x87 support routines to run in the ROM environment. If you don't
have an 8x87 and still want to use floating point, you can't
because the Runtime Library does not include all of the floating
point emulator source code.
SAMPLE PROGRAMS:
A simple C program which reads a value from an input port
and writes a selected string to an output port is included. A
Turbo Make file is provided to create the ROM from this code,
which illustrates all of the necessary processing steps.
STARTUP CODE -- C0ROMX.ASM:
This code is simple, compared to the regular Turbo C startup
code. About all that is required is to set up the DS, SS, and SP
registers. The particular values for these will depend upon the
amount and address space of your RAM. The code has an optional
sequence to initialize static variables to 0, as is "normal" in
C. I did not use such initialization because my particular
application had battery-backed-up static RAM. Note that
C0ROMX.ASM does not have a starting address in its END statement.
The actual code startup is supplied by a JMP inserted by INSJUMP.
Note also that the linking step specifies C0ROMX as the first
object module. That guarantees it will start at relative address
0000 in the ROM.
INSJUMP.C:
The 8088 family processors start up after a RESET by
executing code beginning at FFFF:0. INSJUMP inserts an inter-
segment jump to relative location 0 at the ROM location that
corresponds to FFFF:0. INSJUMP can process 32K (i.e. 27256) or
64K (27512) EPROMs. It is used as follows:
INSJUMP filename [64K]
If the second parameter "64K" is not present, INSJUMP assumes it
is processing a 32K ROM. The JMP instruction is to F800:0, and
it is inserted at location 7FF0 in the ROM. For a 64K ROM, the
instruction is a JMP F000:0 inserted at ROM location FFF0.
The .bin file generated by EXE2BIN may be less than 32K
for a 32K ROM, or less than 64K, for a 64K ROM. INSJUMP
extends the file out to 32K or 64K with FF's. Erased EPROMs
contain all FF's, so unused locations will not have to be
programmed, which will shorten programming time on most
programmers.
As supplied, INSJUMP also calculates a checksum and places
it in the last two ROM bytes. My product has a self-test that
includes ROM checksum on power-up. Other users may want to put
additional data, such as a part number, in the ROM. This can be
done with INSJUMP.
WRITING ASSEMBLY-LANGUAGE FUNCTIONS:
The only differences in writing assembly-language functions
for the ROM environment versus DOS are segment groupings. The
Turbo C User's guide suggests compiling a small C module to
assembler as a starting point. For the ROM environment do the
same thing, then process the assembler through ROMIZE.
If your assembler functions have only a code segment called
_TEXT, with no other segment definitions, and if they reference
only local variables and parameters stored on the stack, then you
can assemble them just once and use the identical .OBJ modules in
both ROM and DOS versions of your main C program.
BINTOHEX.C:
This program converts a binary ROM image to Intel Hex format.
It is has not been tested. If you need some other format, such
as Data I/O, contact me.
This has been a simple treatment of a complicated subject.
I have tried to include as much information as I could think of,
but there are undoubtedly a lot of pitfalls I haven't covered.
Anyone who wishes to pursue this subject further is urged to
contact me:
Bob Haas
P. O. Box 1455
Tualatin, OR 97062-9557
(503) 692-1593
CompuServe User # 70357,3530
Updated 2/2/89 for Turbo C 2.0 by the author.
I recently completed a consulting project to design a
special-purpose controller using an embedded 8088 with firmware
in ROM. To create the firmware, I developed a scheme for
generating ROMable code from Turbo C.
The scheme utilizes the following:
1. Rules for writing C code.
2. ROMIZE.C, a program to post-process the Turbo C Compiler
assembler output.
3. INSJUMP.C, a program to patch the ROM binary code to
insert a restart jump instruction.
4. BINTOHEX.C, a program to produce Intel-format files for
downloading ROM images to an EPROM programmer.
5. C0ROMX.ASM, a replacement for C0.ASM, the startup code.
In addition, the scheme requires the Turboc C command-line
compiler, TCC.EXE; an assembler, Microsoft Version 3.0 or later,
or equivalent (I use Microsoft V5.0); TLINK or MS-Link; and
EXE2BIN (more about EXE2BIN later).
The scheme involves the following steps:
1. C code is written following a few rules.
2. TCC is used to translate the C code to assembly.
3. ROMIZE is used to perform certain modifications to the
assembly code.
4. The modified assembly code is assembled into a .OBJ file.
5. The .OBJ file is linked with the C0ROMX.OBJ startup code
and with other .OBJ files (if needed; these may be from C code
that has gone through the four above steps, or routines
originally coded in assembly). This produces a .EXE file.
6. EXE2BIN converts the .EXE into a binary ROM image.
7. INSJUMP inserts the reset jump into the ROM image.
8. If necessary, the binary ROM image is converted into
whatever format the particular EPROM programmer requires. (Mine
is a plug-in card for the PC which takes binary format directly,
so I don't have to do this step.)
All these steps can be run in a .BAT or MAKE file, making the
conversion of the original C code to ROM code almost automatic.
RULES AND ASSUMPTIONS:
The rules and assumptions for writing ROMable code using this
scheme are as follows:
1. The Compact memory model (small code, large data) is used.
This results in a maximum of 64K of code and initialized static
variables (i.e. constants) but up to 1 MB of Data space.
2. Initialized static variables are considered constants, and
may not be changed during program execution because they are
stored in ROM.
3. Variables defined as "extern" are considered constants.
Such variables would be used for font tables, for example. True
"variables" (which change during program execution) may not be
defined in one C module and declared as "extern" in another. The
only means of inter-module variable passing is through function
parameters and return values (a good programming practice
anyway). IT IS OK to define variables in a C module and reference
these as EXTRN's in assembler-coded modules, however.
4. Most of the Runtime Library routines may NOT be used,
because they have been compiled with the wrong segment groupings
for the ROM environment. Many of the library routines are not
usable, anyway, because they pertain to the DOS environment and
use system facilities that are not available in the ROM
environment. Library routines that are needed will have to be
recreated, either in C or assembly. You may want to order the
Turbo C Runtime Library from Borland as a starting point. I
ordered the Runtime Library several months into the project that
stimulated the creation of this system, but I didn't really need
it.
I was able to compile and debug over 90% of my code (which
was about 3,500 lines of C) using the Integrated Environment on a
PC, then re-compile (with #DEFINES and #IFDEFs to accommodate the
environmental differences) for ROM with little further debugging.
SEGMENT USAGE IN TURBO C:
Turbo C assigns uninitialized static variables to a segment
called _BSS and initialized static variables (i.e., constants) to
a segment called _DATA. These two segments are associated via a
GROUP directive, and assumed, under normal circumstances, to be
jointly addressable by the DS register. Instructions are
generated in the _TEXT segment, and assumed addressable by the CS
register.
In the ROM environment, the constants in the _DATA segment
and the code in the _TEXT segment are both located in the ROM and
addressed by CS. Only BSS is addressed by DS. Therefore to
convert the code generated by the compiler, ROMIZE is used as
follows:
WHAT ROMIZE DOES:
ROMIZE makes two passes through the assembly source code
generated by the command line compiler. In the first pass, it
reads the code and accumulates a table of all symbols defined in
the _DATA segment. There are only a few such symbols, since
Turbo C places all string constants in a single symbol "s@", and
refers to individual strings as "s@+n". Likewise, all
non-string constants are part of a single symbol called "d@" and
referred to by their displacements. There are however, a large
number of references to these symbols in the code.
In its second pass, ROMIZE re-reads the source code and
writes it out with modifications. The modifications are:
1. The command that groups _BSS and DATA together into DGROUP
is changed so that only _BSS is in DGROUP.
2. A new GROUP command is inserted creating CGROUP as _TEXT
and _DATA.
3. The ASSUME statement is changed from
ASSUME cs:_TEXT,ds:DGROUP
to:
ASSUME cs:CGROUP,ds:DGROUP
4. In any line of code with a reference to a symbol in the
_DATA segment, the group reference is changed from DGROUP to
CGROUP, or a CGROUP reference is added if no group name is
present.
5. Preceding a line of code with a reference to a symbol in
the _DATA segment may be a "mov x,ds" or a "push ds"instruction.
This is changed to a "mov x,cs" or "push cs".
6. Following a line of code with a reference to a symbol in
the _DATA segment may be a "push ds" instruction. This is changed
to a "push cs".
In order to do the looking backward and forward, ROMIZE
maintains a three-line pipeline, reading new lines into the head
of the pipe and writing lines from the end.
The various segment naming options of the compiler do not
produce the desired changes for the ROM environment. I would be
happy to pursue this subject further with anyone who is curious
about it.
LINKING:
You can use either TLINK or MS-LINK. The command line will,
of course be different for each. The "Warning - no stack segment"
will be produced by either. This is normal.
You must read the link map carefully to verify that your code
fit in the size of ROM you use. Consider the following partial
link map:
LINK : warning L4021: no stack segment
Start Stop Length Name Class
00000H 05A74H 05A75H _TEXT CODE
05A80H 07DF7H 02378H _DATA DATA
07DF8H 0A530H 02739H _BSS BSS
0A531H 0A531H 00000H _BSSEND BSSEND
Origin Group
0000:0 CGROUP
07DF:0 DGROUP
The .bin file that came out of EXE2BIN for the module that
produced this link map was A531 (hex) bytes long, however, this
module does fit in 32K. The key indicator is the sum of the
lengths of _TEXT and _DATA, and consequently, the starting
address of DGROUP (_BSS). Here it is 7DF8, about 500 bytes
under 32K.
MORE ABOUT EXE2BIN:
EXE2BIN is needed to convert the .EXE file into a binary
image that can be transmitted to an EPROM programmer. It may need
to perform "segment fixups" (segment relocation), if your program
includes constructs like the following:
static char *menu[] = {"Preview ","Add Before","Add After "};
This is a convenient way to generate arrays of strings for
such things as menus. This generates an array of pointers whose
segment parts must be relocated by EXE2BIN to the segment address
of the ROM. Therefore, when the EXE2BIN step is run, you will be
prompted to enter a segment address (F800 hex in the attached
example).
For those users of versions of DOS (such as 3.3) that do not
include EXE2BIN, there are several alternatives: Use EXE2BIN from
DOS 2.0 (I think 2.1 will work also), or use EXE2BIN from DOS 3.0
or 3.1 and patch it so it does not test for DOS version. The test
is at the beginning of these programs and is easily defeated.
Except for the version check, I know of no DOS version
dependencies in these programs. (I did all my development under
DOS 3.2, however, which still includes EXE2BIN).
The EXE2COM program available on CompuServe and many bulletin
boards may work in place of EXE2BIN (I have not tested it),
EXCEPT when segment relocation is required (i.e., you use the
above-described construct). It does not do segment relocation.
The fact that EXE2BIN stops and prompts you for a segment
relocation factor is an annoyance. You can use the PC Magazine
KEY-FAKE.COM (see PCMagnet) to supply the required value in a
batch or MAKE file. If KEY-FAKE is used in a MAKE file, you must
have invoked KEY-FAKE at least once (without arguments is fine)
before running MAKE. KEY-FAKE installs as a TSR the first time
it is invoked, and apparently, Turbo MAKE doesn't like programs
that do this, so if you invoke KEY-FAKE in a MAKE file
without it already being resident Turbo MAKE terminates with an
error 768.
MORE HINTS:
If you declare an array of strings as mentioned in the above
discussion, the "static" storage class is not necessary if the
array is defined outside of any function. If such an array is
defined as local to (within) a function, then be certain to add
"static", otherwise, Turbo C generates code to copy the array
into the function's local variable space (the stack). This is
unnecessary if the array is indeed a constant.
Although you won't be using many (any) library functions, you
may still want to include a number of standard header files.
The library functions you recreate ought to use the same
definitions as the library versions, anyway.
"dos.h" is an especially useful item. Although chances are
you won't be using any of the file I/O functions defined in it,
it includes some macros and in-line functions that are EXTREMELY
useful in the ROM environment such as MK_FP(), inport(),
outport(), FP_SEG(), FP_OFF(), peek() and poke().
FLOATING POINT LIMITATIONS:
If you need to use floating point arithmetic, you may have a
problem. If your target hardware includes a numeric co-processor
then you will have to order the Runtime Library and modify the
8x87 support routines to run in the ROM environment. If you don't
have an 8x87 and still want to use floating point, you can't
because the Runtime Library does not include all of the floating
point emulator source code.
SAMPLE PROGRAMS:
A simple C program which reads a value from an input port
and writes a selected string to an output port is included. A
Turbo Make file is provided to create the ROM from this code,
which illustrates all of the necessary processing steps.
STARTUP CODE -- C0ROMX.ASM:
This code is simple, compared to the regular Turbo C startup
code. About all that is required is to set up the DS, SS, and SP
registers. The particular values for these will depend upon the
amount and address space of your RAM. The code has an optional
sequence to initialize static variables to 0, as is "normal" in
C. I did not use such initialization because my particular
application had battery-backed-up static RAM. Note that
C0ROMX.ASM does not have a starting address in its END statement.
The actual code startup is supplied by a JMP inserted by INSJUMP.
Note also that the linking step specifies C0ROMX as the first
object module. That guarantees it will start at relative address
0000 in the ROM.
INSJUMP.C:
The 8088 family processors start up after a RESET by
executing code beginning at FFFF:0. INSJUMP inserts an inter-
segment jump to relative location 0 at the ROM location that
corresponds to FFFF:0. INSJUMP can process 32K (i.e. 27256) or
64K (27512) EPROMs. It is used as follows:
INSJUMP filename [64K]
If the second parameter "64K" is not present, INSJUMP assumes it
is processing a 32K ROM. The JMP instruction is to F800:0, and
it is inserted at location 7FF0 in the ROM. For a 64K ROM, the
instruction is a JMP F000:0 inserted at ROM location FFF0.
The .bin file generated by EXE2BIN may be less than 32K
for a 32K ROM, or less than 64K, for a 64K ROM. INSJUMP
extends the file out to 32K or 64K with FF's. Erased EPROMs
contain all FF's, so unused locations will not have to be
programmed, which will shorten programming time on most
programmers.
As supplied, INSJUMP also calculates a checksum and places
it in the last two ROM bytes. My product has a self-test that
includes ROM checksum on power-up. Other users may want to put
additional data, such as a part number, in the ROM. This can be
done with INSJUMP.
WRITING ASSEMBLY-LANGUAGE FUNCTIONS:
The only differences in writing assembly-language functions
for the ROM environment versus DOS are segment groupings. The
Turbo C User's guide suggests compiling a small C module to
assembler as a starting point. For the ROM environment do the
same thing, then process the assembler through ROMIZE.
If your assembler functions have only a code segment called
_TEXT, with no other segment definitions, and if they reference
only local variables and parameters stored on the stack, then you
can assemble them just once and use the identical .OBJ modules in
both ROM and DOS versions of your main C program.
BINTOHEX.C:
This program converts a binary ROM image to Intel Hex format.
It is has not been tested. If you need some other format, such
as Data I/O, contact me.
This has been a simple treatment of a complicated subject.
I have tried to include as much information as I could think of,
but there are undoubtedly a lot of pitfalls I haven't covered.
Anyone who wishes to pursue this subject further is urged to
contact me:
Bob Haas
P. O. Box 1455
Tualatin, OR 97062-9557
(503) 692-1593
CompuServe User # 70357,3530
December 11, 2017
Add comments