PS2 DMAC Basics
The DMAC for dummies.
I’d like to gently explain what the DMAC is and provide an example of its basic usage.
DMAC stands for Direct Memory Access Controller. It connects the different components (GS,VU1,IOP) to the main memory which in turn allows the EE to multitask. Absolutely every piece of software for the PS2 uses the DMAC. It can transfer 16 bytes (a quadword) of data every 2 EE clock cycles.
The DMAC has 10 “channels” or “paths” that point to various parts of the system.
Channel No | Component | Direction |
---|---|---|
0 | VIF0 | TO |
1 | VIF1 | BIDIR |
2 | GIF | TO |
3 | IPU | FROM |
4 | IPU_TO | TO |
5 | SIF0* | FROM |
6 | SIF1* | TO |
7 | SIF2* | BIDIR |
8 | SPR | FROM |
9 | SPR | TO |
*The SIF controls the communication between the EE and IOP.
Every channel excluding the SPR ones expect data to be in special formats. To remove the complexities that are involved in these formats, I will use the SPR channels for examples.
The SPR
SPR stands for ScratchPad RAM. Think of it as a seperate block of fast memory for the EE. Here is a modified block diagram of all that is needed for SPR memory access.
Now, I feel like it’s important to note that are two ways to read/write to SPR.
- MMU mapped memory segment
- DMAC channels
The MMU mapped memory may sound complicated, but if you can understand basic C it’s very easy.
// SPR memory starts at address 0x70000000
u8* SPR_MEMORY = 0x70000000;
// Write some data to the SPR
SPR_MEMORY[0] = 0xFF;
SPR_MEMORY[1] = 0x00;
SPR_MEMORY[2] = 0xFF;
SPR_MEMORY[3] = 0x00;
// Congrats, you've just used SPR memory!
If we theoretically wanted to copy an entire texture to SPR we’d do the following.
u32 texture_to_transfer_size_qw = 0;
qword_t* texture_to_transfer = get_next_texture(&texture_to_transfer_size_qw);
process_texture(texture_to_transfer);
u128* SPR_MEMORY = 0x70000000;
for(int i = 0; i < texture_to_transfer_size_qw; i++)
{
SPR_MEMORY[i] = texture_to_transfer[i];
}
free_texture(texture_to_transfer);
This is fine, it works, but the EE is stuck spinning in a loop transferring memory around. What if we let the DMAC take care of that while we continue to use the EE?
Finally. How to Use the DMAC
Each DMAC channel has the following registers.
- CHCR (Channel Control)
- MADR (Memory Address)
- TADR (Tag Address)
- QWC (QWORD Count)
The SPR channels have a special register.
- SADR (SPR Address)
A summary of how it works is as follows:
- Set MADR to your source/dest (TO/FROM channel respectively)
- Set QWC to the number of qwords (16 bytes) you want to transfer
- Set bit 8 (0x100) of CHCR to start your transfer
- Once bit 8 of CHCR is 0, the transfer has completed
Here’s an example of the theoretical texture upload using the DMAC. (I’ve simplified the channel register names. The actual ones can be located here).
-
u32 texture_to_transfer_size_qw = 0; qword_t* texture_to_transfer = get_next_texture(&texture_to_transfer_size_qw); process_texture(texture_to_transfer); // Point to the start of SPR *SPR_CHANNEL_SADR = 0x00; // Point to the start of our texture buffer *SPR_CHANNEL_MADR = texture_to_transfer; *SPR_CHANNEL_QWC = texture_to_transfer_size_qw; // Start the transfer *SPR_CHANNEL_CHCR = CHCR_STR; // Wait for the DMAC to finish while(*SPR_CHANNEL_CHCR & CHCR_STR) {}; free_texture(texture_to_transfer); // Somehow use the data in SPR
-
u32 texture_to_transfer_size_qw = 0; qword_t* texture_to_transfer = get_next_texture(&texture_to_transfer_size_qw); process_texture(texture_to_transfer); // libdma has no way to set SADR registers // Point to the start of SPR *SPR_CHANNEL_SADR = 0x00; // Start the transfer dma_channel_send_normal(DMA_CHANNEL_toSPR, texture_to_transfer, texture_to_transfer_size_qw, 0, 0); // Wait for the channels transfer to end dma_channel_wait(DMA_CHANNEL_toSPR, 0); free_texture(texture_to_transfer); // Somehow use the data in SPR
This does exactly what the MMU mapping example does above. Not very impressive. But! We can now incorporate some optimizations to keep the EE busy.
-
qword_t* transferred_texture = NULL; while(1) { u32 next_texture_size_qw = 0; qword_t* next_texture = get_next_texture(&next_texture_size_qw); if(next_texture == NULL) break; process_texture(next_texture); // Ensure that the DMA channel is not busy while(*SPR_CHANNEL_CHCR & CHCR_STR) {}; if(transferred_texture != NULL) { // Somehow use the data in SPR free_texture(transferred_texture); } // Point to the start of SPR *SPR_CHANNEL_SADR = 0x00; // Point to the start of our texture buffer *SPR_CHANNEL_MADR = next_texture; *SPR_CHANNEL_QWC = next_texture_size_qw; // Start the transfer *SPR_CHANNEL_CHCR = CHCR_STR; transferred_texture = next_texture; } // Somehow use the data in SPR free_texture(transferred_texture);
-
qword_t* transferred_texture = NULL; while(1) { u32 next_texture_size_qw = 0; qword_t* next_texture = get_next_texture(&next_texture_size_qw); if(next_texture == NULL) break; process_texture(next_texture); // Ensure that the DMA channel is not busy dma_channel_wait(DMA_CHANNEL_toSPR, 0); if(transferred_texture != NULL) { // Somehow use the data in SPR free_texture(transferred_texture); } // libdma has no way to set SADR registers // Point to the start of SPR *SPR_CHANNEL_SADR = 0x00; dma_channel_send_normal(DMA_CHANNEL_toSPR, next_texture, next_texture_size_qw, 0, 0); transferred_texture = next_texture; } // Somehow use the data in SPR free_texture(transferred_texture);
In this example we can load our next texture and do any processing to it while the previous texture is still uploading. This concept is known as double buffering.
While this post has covered the basics of the DMAC. For those interested in digging deeper into PS2 development, I encourage you to try DMATags. In most cases it’s a free optimzation. This is something I’m interested in covering next.
DMAC Notes
Caching was omitted from this post, you must ensure that you’ve flushed the data cache
before starting a transfer.
Alignment is a concern. MADR must be qword aligned. You can dynamically allocate
aligned data by using aligned_alloc. You can align a type by using __attribute__ ((aligned (16)));