foo(i + 2 - 1);lcc will add 2 and substract 1, instead of just adding 1. In this situation, you should parenthesize then constant expression:
foo(i + (2 - 1));
type.h
):
BYTE UBYTE WORD UWORD LWORD ULWORDor
INT8 UINT8 INT16 UINT16 INT32 UINT32That way, you will always know the size of your variables.
U
, L
and UL
postfixes when necessary. U
specifies that the constant is unsigned, while L
specifies that the constant is long. Consider the following example:
UBYTE i, j = 0; i = j+0x80;The compiler will think that
0x80
is a signed value, and since it is bigger than the biggest signed 8-bit value (0x79), the compiler will treat it as a long constant. Following the C specification, j
will be extended to a long, and added to 0x80
using a long addition (which is costly on an 8-bit processor), before beeing truncated to an 8-bit value and assigned to i
. This example should be written:
UBYTE i, j = 0; i = j+0x80U;
int i1; /* OK : will be located in RAM */ char *s1; /* OK : will be located in RAM */ int i2 = 0; /* Error : will be located in ROM */ char *s2 = "Hi"; /* Error : will be located in ROM */ void main() { ... }
==
and !=
comparison operators to <
, <=
, >
, and >=
. The code will be shorter and quicker. For instance:
for(i = 0; i < 10; i++)is less efficient than
for(i = 0; i != 10; i++)
int
variables to long
if they have to be bigger than 255. If they should only contain values between 0 and 255, use an unsigned int
.
delay
function, you'll have to adapt your delay values.
show_bkg()
is now SHOW_BKG
).
For mixing C and assembly, you must use one file per language (you cannot embed C code with assembly) and link both files together. Here are the things to know:
i
will be called _i
in assembly.
DE
register.
SP+2
because the return address is also saved on the stack).
.globl
directive.
_reg_0xXX
where XX
is the register number (see sound.c
for an example).
HL
(and DE
when the function returns a result).
Here is an example of how to mix assembly with C:
main.c
main() { WORD i; WORD add(WORD, WORD); i = add(1, 3); }
add.s
.globl _add _add: ; WORD add(WORD a, WORD b) ; There is no register to save: ; BC is not used ; DE is the return register ; HL needs never to be saved LDA HL,2(SP) LD E,(HL) ; Get a in DE INC HL LD D,(HL) INC HL LD A,(HL) ; Get b in HL INC HL LD H,(HL) LD L,A ADD HL,DE ; Add DE to HL LD D,H LD E,L ; There is no register to restore RET ; Return result in DE
GBDK can generate multiple bank images (with both multible ROM and RAM banks) for MBC1 and MBC2 memory bank controllers. Multiple RAM banks are only supported by MBC 1.
With multiple ROM banks, addresses 0x0000 to 0x3FFF are reserved for the fixed ROM bank, while addresses 0x4000 to 0x7FFF are switchable, i.e. can be used for any bank. Switchable ROM banks are called _CODE_1
, _CODE_2
,... and the fixed ROM bank is called _CODE
(note that there is no _CODE_0
). The maximum number of ROM banks is 32.
Addresses 0xC000 to 0xDFFF are always reserved for the internal RAM. Addresses 0xA000 to 0xBFFF are reserved for (switchable) external RAM. External RAM banks are called _BSS_0
, _BSS_1
, _BSS_2
,... and internal RAM is called _BSS
. The maximum number of external RAM banks is 4.
When deciding how to populate your RAM banks, remember that local variables are always allocated on the stack, and initialized global variables are located in ROM. Only uninitialized global or static variables are allocated into RAM.
For generating multiple bank images, you have to:
-Wf-bo#
flag (where #
is the number of the bank, greater than 0). If you do not use this flag, the code will be located in the fixed ROM bank.
-Wf-ba#
flag (where #
is the number of the bank, greater or equal to 0). If you do not use this flag, the data will be located in the internal RAM.
-Wl-yo#
(for ROM) and -Wl-ya#
(for RAM) flags (where #
is the number of banks), and the type of MBC used in the cartridge using the -Wl-yt#
flag (where #
is the cartridge type code to be located at address 0x147 of the image).256Kbit = 32KByte = 2 banks 512Kbit = 64KByte = 4 banks 1Mbit = 128KByte = 8 banks 2Mbit = 256KByte = 16 banks 4Mbit = 512KByte = 32 banksStandard supported RAM sizes are:
None 64kBit = 8kB = 1 bank 256kBit = 32kB = 4 banksStandard supported cartridge types are:
0 : ROM ONLY 1 : ROM+MBC1 2 : ROM+MBC1+RAM 3 : ROM+MBC1+RAM+BATTERY 5 : ROM+MBC2 6 : ROM+MBC2+BATTERY
Bank switching is not automatic in programs. You have to explicitely call the switch_rom_bank()
and switch_ram_bank()
functions. See banks.c
for a complete example.
It is possible to copy functions to RAM and HIRAM (using the memcpy()
and hiramcpy()
functions), and execute them from C. The compiler automatically generates two symbol for the start and the end of each function, named start_X
and end_X
(where X
is the name of the function). This enables to calculate the length of a function when copying it to RAM. Ensure you have enough free space in RAM or HIRAM for copying a function.
There are basically two ways for calling a function located in RAM, HIRAM, or ROM:
extern
, and set its address at link time using the -Wl-gXXX=#
flag (where XXX
is the name of the function, and #
is its address).
The second approach is slightly more efficient. Both approaches are illustrated in the ram_fn.c
example.
The GameBoy hardware can generate 5 types of interrupts:
VBL : V-blank LCD : LCDC status TIM : Timer overflow SIO : Serial I/O transfer end JOY : Transition from high to low of joypad
It is possible to install your own interrupt handlers (in C or in assembly) for any of these interrupts. Up to 7 interrupt handlers can be installed for each interrupt. Interrupt handlers are called in sequence. To install a new interrupt handler, do the following:
foo()
) that takes no parameter, and that returns nothing. Remember that the code executed in an interrupt handler must be short.
add_XXX()
function, where XXX
is the interrupt that you want to handle.
set_interrupts()
function. Note that the VBL interrupt is already enabled before the main()
function is called. If you want to set the interrupts before main()
is called, you must install an initialization routine.
See irq.c
for a complete example.
You can install a routine that will be executed before the main()
function is called, and just before the interrupts are enabled. For instance, you can use an initialization routine to modify the interrupt flags and avoid that a VBL IRQ is handled before main()
is executed. For installing an initialization routine, you have to:
foo()
) that takes no parameter, and that returns nothing.
-Wl-g.init=XXX
flag (where XXX
is the name of your function). Remember that your function will have an initial underscore in assembly. In our example, it will be -Wl-g.init=_foo
.
It is possible to change the addresses of some important data at link time using the -Wl-gXXX=YYY
flag (where XXX
is the name of the data, and YYY
is the new address). The addresses that can be changed are:
.OAM : Location of sprite ram (requires 0xA0 bytes) .STACK : Initial stack address .refresh_OAM : Address to which the routine for refreshing OAM will be copied (must be in HIRAM) .init : Initialization routine