Not really sure where to put this, so I'll put it here. There's been some interest as to how I'm using SDCC to program
for the TI-84+CSE, so I figured I'd make a post about it.
1. Install SDCC. I will be using SDCC version 3.4.0 (according to sdcc --version)
I'm not really sure how to do this on Windows. On Arch I was able to run 'pacman -S sdcc' and everything just worked.
2. Create a project folder. Go get a copy of ti84pcse.inc and put it in the project folder.
3. To follow this tutorial, get Brass and put it in your project folder
4. Make yourself a C file called sdcc_test.c. This is a good test file to make sure that you've got something running
Code:
5. Make a wrapper assembly program called sdcc_test_wrapper.asm (this is how I'm doing things right now, there are
alternatives) Here's a nostub assembly program that should assemble with Brass. It expects your output binary it's
including to be called sdcc_test.bin (we'll get to that later), and it also expects a ti84pcse.inc file in the same
folder as it.
Code:
6. Compile your C code! You should be able to use the following command.
Code:
Let's go over the arguments!
-mz80 : Tells SDCC to compile for the z80 processor
--code-loc 42507 : think of this like '.org' in assembly. It tells SDCC where your code will be. 42507 = UserMem on the CSE
--no-std-crt0 : SDCC includes a standard 'crt0' wrapper around the output by default. You can make your own (which I think
I'll need to do later for reasons I'll mention later) if you want and provide it as well, but this by itself is enough
to get SDCC to output just the data for the code we've written ourselves.
--data-loc 0 : I'm not really sure what this does honestly. I don't even know if it's necessary. I recommend reading the
official documentation if you're curious
--reserve-regs-iy : This makes it so SDCC will never touch the IY register, which is important if you do anything touching
system flags. You can omit this if you make sure to set IY to flags before b_calling something that might use it, and
you make sure to set IY to flags before returning
--max-allocs-per-node 30000 : I'm not entirely sure exactly what this affects in the compiler. The higher it is, the
longer your code will take to compile, but the more optimized it will be. the default is 3000. I've kept it at 30000
because it was the first number I tried, and it made the code significantly more optimized (much smaller file size, even
when optimizing for speed). Maybe try experimenting to find a good tradeoff between compilation speed and optimization.
sdcc_test.c : The C file you're compiling.
This will generate a bunch of intermediary files as a result of the compilation, but the main one we care about is
sdcc_test.ihx. This is a file containing your code in intel hex format. To get this into a raw binary, I use the hex2bin
program with the following command which will generate sdcc_test.bin:
Code:
I'm not sure what alternatives there are to hex2bin. Really you can just write your own if you want, the intel hex
format isn't that complicated.
7. Finally, we can assemble the wrapper assembly program we wrote earlier:
Code:
Transfer SDCCTEST.8xp to an emulator or your hardware and you should be able to run it. It should look something like this:
As promised a few additional details:
► If you define global variables, they will be inserted as empty space in your program BEFORE main(). This means
that the first time your program runs you'll NOP sled down to main(), but if you're using a shell with writeback there's
no telling what will happen the next time it runs. I think that you can jump past them to main() if you provide a custom
replacement for the standard crt0 file. I have not tried doing this yet as I haven't needed to. I've instead defined all
my global variables to be located in the typical freeram locations assembly programmers know and love, using the __at()
syntax shown in the above sample C file.
► If you want to use or display a string whose value will never change, declare it globally and declare it const.
Otherwise SDCC will generate some nasty code to construct the string on the fly when used. Doing this sort of thing
should be familiar to Axe devs. the 'const' thing goes for ANY global data you define which you are not going to change.
All 'const' globals are inserted AFTER your code in the resulting assembly instead of BEFORE.
Example: const char SOME_STR[] = "Hello World!";
► If you want to write custom assembly routines and call them from C, the SDCC manual has a good reference on how to do
this (link to manual PDF below). However there are a few gotchas. The manual will mention that parameters are passed by
writing the values to fixed memory addresses unless a function is marked 'reentrant', but this is not true of the z80
compiler. The z80 compiler ALWAYS passes values on the stack. Also, if you (or routines you call) modify IX, you must
restore it's value before returning because SDCC uses IX to maintain the stack frame. The manual also has a section with
details related specifically to z80 assembly routines, the most important bit of information is: Return 1 byte values in
L, 2 byte values in HL, 4 byte values in DEHL.
► Local variables are stored on the stack. Be careful of declaring a lot of local variables, having a deep call stack,
calling a function recursively too much, or really anything that would cause a lot of memory allocation on the stack.
The stack, by default, does not really have much space available to it. If you DO need to do these things, look into
moving the stack somewhere else (I've moved it to saveSScreen before with no problems). If you want to do this, make
sure you know EXACTLY what you're doing though. Any code which swaps out the page of ram you put the stack on will
probably crash the calculator. Moving the stack pointer is not something to be taken lightly.
► The SDCC manual PDF can be found here: http://sdcc.sourceforge.net/doc/sdccman.pdf
Big thanks to AHelper for answering my questions related to the assembler in IRC, and for making
this thread a while ago which helped me figure out a few
things I didn't figure out from reading the manual. You can also read a bit about the whole 'crt0' thing there too,
though I don't know how relevant it is as I haven't really read it myself.
for the TI-84+CSE, so I figured I'd make a post about it.
1. Install SDCC. I will be using SDCC version 3.4.0 (according to sdcc --version)
I'm not really sure how to do this on Windows. On Arch I was able to run 'pacman -S sdcc' and everything just worked.
2. Create a project folder. Go get a copy of ti84pcse.inc and put it in the project folder.
3. To follow this tutorial, get Brass and put it in your project folder
4. Make yourself a C file called sdcc_test.c. This is a good test file to make sure that you've got something running
Code:
//You can't write the code for any functions above main(), or else
//the top function defined will run instead of main(). The top
//function defined in your file must be the code you want to run
//when your program starts. I think a custom crt0 file would allow
//you to avoid this problem
void disp_hl(unsigned int);
void getkey();
//Define variables curRow/curCol to exist at specific addressed
//These are the addresses of the actual OS curRow/curCol for _DispHL
//If you get tired of typing 'unsigned' before all your chars,
//there's a compiler for '--funsigned-char' which will make all
//chars unsigned by default, unless you explicity write 'signed'
__at(0x8459) unsigned char curRow;
__at(0x845A) unsigned char curCol;
//Can be 'int main' but nothing is using your return value anyway
void main(void)
{
//Set output position on screen
curRow = 2;
curCol = 3;
//Should display 32325
disp_hl(32325);
//Wait for key press
getkey();
}
//Really you should write this in it's own assembly file
//and declare it as extern up here. I'm just defining
//it like this so everything fits in one file
//
//an 'int' is 2 bytes.
void disp_hl(unsigned int x)
{
//x should be in HL
//We reference it below to avoid compiler warnings
x;
__asm
push hl
rst 0x28
.dw 0x453D ;B_CALL(_ClrScrnFull)
pop hl
rst 0x28
.dw 0x44FE ;B_CALL(_DispHL) on CSE
__endasm;
}
//does a B_CALL(_GetKey) to pause.
//Doesn't return the key value
void getkey() {
__asm
rst 0x28
.dw 0x495D ;B_CALL(_GetKey)
__endasm;
}
5. Make a wrapper assembly program called sdcc_test_wrapper.asm (this is how I'm doing things right now, there are
alternatives) Here's a nostub assembly program that should assemble with Brass. It expects your output binary it's
including to be called sdcc_test.bin (we'll get to that later), and it also expects a ti84pcse.inc file in the same
folder as it.
Code:
.binarymode TI8X
.variablename "SDCCTEST"
.nolist
.global
#include "ti84pcse.inc"
.endglobal
.list
.org UserMem - 2
.db tExtTok, tAsm84CCmp
.incbin "sdcc_test.bin"
6. Compile your C code! You should be able to use the following command.
Code:
sdcc -mz80 --code-loc 42507 --no-std-crt0 --data-loc 0 --reserve-regs-iy --max-allocs-per-node 30000 sdcc_test.c
Let's go over the arguments!
-mz80 : Tells SDCC to compile for the z80 processor
--code-loc 42507 : think of this like '.org' in assembly. It tells SDCC where your code will be. 42507 = UserMem on the CSE
--no-std-crt0 : SDCC includes a standard 'crt0' wrapper around the output by default. You can make your own (which I think
I'll need to do later for reasons I'll mention later) if you want and provide it as well, but this by itself is enough
to get SDCC to output just the data for the code we've written ourselves.
--data-loc 0 : I'm not really sure what this does honestly. I don't even know if it's necessary. I recommend reading the
official documentation if you're curious
--reserve-regs-iy : This makes it so SDCC will never touch the IY register, which is important if you do anything touching
system flags. You can omit this if you make sure to set IY to flags before b_calling something that might use it, and
you make sure to set IY to flags before returning
--max-allocs-per-node 30000 : I'm not entirely sure exactly what this affects in the compiler. The higher it is, the
longer your code will take to compile, but the more optimized it will be. the default is 3000. I've kept it at 30000
because it was the first number I tried, and it made the code significantly more optimized (much smaller file size, even
when optimizing for speed). Maybe try experimenting to find a good tradeoff between compilation speed and optimization.
sdcc_test.c : The C file you're compiling.
This will generate a bunch of intermediary files as a result of the compilation, but the main one we care about is
sdcc_test.ihx. This is a file containing your code in intel hex format. To get this into a raw binary, I use the hex2bin
program with the following command which will generate sdcc_test.bin:
Code:
#A60B is also UserMem
hex2bin -s A60B sdcc_test.ihx
I'm not sure what alternatives there are to hex2bin. Really you can just write your own if you want, the intel hex
format isn't that complicated.
7. Finally, we can assemble the wrapper assembly program we wrote earlier:
Code:
#just 'Brass.exe' on windows
mono Brass.exe sdcc_test_wrapper.asm SDCCTEST.8xp
Transfer SDCCTEST.8xp to an emulator or your hardware and you should be able to run it. It should look something like this:
As promised a few additional details:
► If you define global variables, they will be inserted as empty space in your program BEFORE main(). This means
that the first time your program runs you'll NOP sled down to main(), but if you're using a shell with writeback there's
no telling what will happen the next time it runs. I think that you can jump past them to main() if you provide a custom
replacement for the standard crt0 file. I have not tried doing this yet as I haven't needed to. I've instead defined all
my global variables to be located in the typical freeram locations assembly programmers know and love, using the __at()
syntax shown in the above sample C file.
► If you want to use or display a string whose value will never change, declare it globally and declare it const.
Otherwise SDCC will generate some nasty code to construct the string on the fly when used. Doing this sort of thing
should be familiar to Axe devs. the 'const' thing goes for ANY global data you define which you are not going to change.
All 'const' globals are inserted AFTER your code in the resulting assembly instead of BEFORE.
Example: const char SOME_STR[] = "Hello World!";
► If you want to write custom assembly routines and call them from C, the SDCC manual has a good reference on how to do
this (link to manual PDF below). However there are a few gotchas. The manual will mention that parameters are passed by
writing the values to fixed memory addresses unless a function is marked 'reentrant', but this is not true of the z80
compiler. The z80 compiler ALWAYS passes values on the stack. Also, if you (or routines you call) modify IX, you must
restore it's value before returning because SDCC uses IX to maintain the stack frame. The manual also has a section with
details related specifically to z80 assembly routines, the most important bit of information is: Return 1 byte values in
L, 2 byte values in HL, 4 byte values in DEHL.
► Local variables are stored on the stack. Be careful of declaring a lot of local variables, having a deep call stack,
calling a function recursively too much, or really anything that would cause a lot of memory allocation on the stack.
The stack, by default, does not really have much space available to it. If you DO need to do these things, look into
moving the stack somewhere else (I've moved it to saveSScreen before with no problems). If you want to do this, make
sure you know EXACTLY what you're doing though. Any code which swaps out the page of ram you put the stack on will
probably crash the calculator. Moving the stack pointer is not something to be taken lightly.
► The SDCC manual PDF can be found here: http://sdcc.sourceforge.net/doc/sdccman.pdf
Big thanks to AHelper for answering my questions related to the assembler in IRC, and for making
this thread a while ago which helped me figure out a few
things I didn't figure out from reading the manual. You can also read a bit about the whole 'crt0' thing there too,
though I don't know how relevant it is as I haven't really read it myself.