Class: PSX::DMA

Inherits:
Object
  • Object
show all
Defined in:
lib/psx/dma.rb

Overview

DMA Controller Handles bulk memory transfers between RAM and devices

Defined Under Namespace

Classes: Channel

Constant Summary collapse

MDEC_IN =

DMA Channels

0
MDEC_OUT =

MDEC decoder input

1
GPU =

MDEC decoder output

2
CDROM =

Graphics Processing Unit

3
SPU =

CD-ROM drive

4
PIO =

Sound Processing Unit

5
OTC =

Expansion port

6
NUM_CHANNELS =

Ordering Table Clear

7
CTRL_DIRECTION =

Channel control register bits

0x0000_0001
CTRL_STEP =

0=To RAM, 1=From RAM

0x0000_0002
CTRL_CHOPPING =

0=Forward (+4), 1=Backward (-4)

0x0000_0100
CTRL_SYNC_MODE =

Enable chopping

0x0000_0600
CTRL_CHOP_DMA =

Sync mode (bits 9-10)

0x0007_0000
CTRL_CHOP_CPU =

Chopping DMA window (bits 16-18)

0x0700_0000
CTRL_START_BUSY =

Chopping CPU window (bits 20-22)

0x0100_0000
CTRL_START_TRIGGER =

Start/Busy (bit 24)

0x1000_0000
SYNC_MANUAL =

Sync modes

0
SYNC_REQUEST =

Transfer all at once (burst)

1
SYNC_LINKED =

Sync blocks to DRQ

2
DICR_FORCE_IRQ =

DICR bits

0x0000_8000
DICR_IRQ_ENABLE =

Force IRQ (bit 15)

0x007F_0000
DICR_IRQ_MASTER =

Per-channel IRQ enable (bits 16-22)

0x0080_0000
DICR_IRQ_FLAGS =

Master IRQ enable (bit 23)

0x7F00_0000
DICR_IRQ_MASTER_FLAG =

Per-channel IRQ flags (bits 24-30)

0x8000_0000

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(interrupts: nil, spu: nil) ⇒ DMA

Returns a new instance of DMA.



101
102
103
104
105
106
107
108
# File 'lib/psx/dma.rb', line 101

def initialize(interrupts: nil, spu: nil)
  @interrupts = interrupts
  @spu = spu
  @channels = Array.new(NUM_CHANNELS) { Channel.new }
  @dpcr = 0x0765_4321  # Default: all channels enabled with default priorities
  @dicr = 0
  @master_flag_latched = false  # Has IRQ#3 fired for the current rising edge?
end

Instance Attribute Details

#channelsObject (readonly)

Returns the value of attribute channels.



98
99
100
# File 'lib/psx/dma.rb', line 98

def channels
  @channels
end

#dicrObject (readonly)

Returns the value of attribute dicr.



98
99
100
# File 'lib/psx/dma.rb', line 98

def dicr
  @dicr
end

#dpcrObject (readonly)

Returns the value of attribute dpcr.



98
99
100
# File 'lib/psx/dma.rb', line 98

def dpcr
  @dpcr
end

#spuObject

Returns the value of attribute spu.



99
100
101
# File 'lib/psx/dma.rb', line 99

def spu
  @spu
end

Instance Method Details

#channel_enabled?(n) ⇒ Boolean

Returns:

  • (Boolean)


210
211
212
213
214
# File 'lib/psx/dma.rb', line 210

def channel_enabled?(n)
  # Check DPCR enable bit for channel
  # Each channel has 4 bits in DPCR, bit 3 of each is enable
  (@dpcr >> (n * 4 + 3)) & 1 == 1
end

#read(offset) ⇒ Object

Register access (offset from 0x1F801080)



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/psx/dma.rb', line 111

def read(offset)
  if offset < 0x70
    # Channel registers
    channel_num = offset >> 4
    reg = offset & 0xF

    return 0 if channel_num >= NUM_CHANNELS

    channel = @channels[channel_num]
    case reg
    when 0x0 then channel.base_addr
    when 0x4 then channel.block_ctrl
    when 0x8 then channel.channel_ctrl
    else 0
    end
  elsif offset == 0x70
    @dpcr
  elsif offset == 0x74
    @dicr
  else
    0
  end
end

#set_irq_flag(channel) ⇒ Object



216
217
218
219
220
221
222
# File 'lib/psx/dma.rb', line 216

def set_irq_flag(channel)
  # Per Nocash: per-channel IRQ flag is set on completion only when the
  # corresponding per-channel IRQ enable (bits 16-22) is also set.
  return unless ((@dicr >> (16 + channel)) & 1) == 1
  @dicr |= (1 << (24 + channel))
  update_master_flag
end

#tick(memory, gpu: nil) ⇒ Object



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/psx/dma.rb', line 242

def tick(memory, gpu: nil)
  NUM_CHANNELS.times do |n|
    next unless channel_enabled?(n)
    sync_mode = (@channels[n].channel_ctrl >> 9) & 0x3
    # Only OTC needs an explicit manual trigger in SyncMode 0; channels
    # backed by a device (GPU/SPU/...) start on BUSY alone.
    needs_trigger = (n == OTC) && (sync_mode == SYNC_MANUAL)
    next unless @channels[n].active?(needs_trigger: needs_trigger)

    case n
    when GPU
      transfer_gpu(memory, gpu)
    when SPU
      transfer_spu(memory)
    when OTC
      transfer_otc(memory)
    # Other channels can be added as needed
    end
  end
end

#tick_cycles(cycles) ⇒ Object

Execute pending DMA transfers Returns true if any transfer was performed



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/psx/dma.rb', line 226

def tick_cycles(cycles)
  return unless @pending_completions && !@pending_completions.empty?

  @pending_completions.reject! do |n|
    ch = @channels[n]
    ch.busy_cycles -= cycles
    if ch.busy_cycles <= 0
      ch.finish!
      set_irq_flag(n)
      true
    else
      false
    end
  end
end

#update_master_flagObject



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/psx/dma.rb', line 186

def update_master_flag
  # PSX semantics: IRQ#3 fires on the rising edge of master-flag (bit 31).
  # The master flag itself is "calculated" from current state. We expose it
  # as DICR bit 31 for software to read, and use an internal latch so the
  # IRQ fires exactly once per rising edge — until the condition goes false
  # again (BIOS acks channel flags or clears master enable).
  force = (@dicr & DICR_FORCE_IRQ) != 0
  master_enable = (@dicr & DICR_IRQ_MASTER) != 0
  flags = (@dicr >> 24) & 0x7F
  enables = (@dicr >> 16) & 0x7F
  master = force || (master_enable && (flags & enables) != 0)

  if master
    @dicr |= DICR_IRQ_MASTER_FLAG
    unless @master_flag_latched
      @master_flag_latched = true
      @interrupts&.request(Interrupts::IRQ_DMA)
    end
  else
    @dicr &= ~DICR_IRQ_MASTER_FLAG
    @master_flag_latched = false
  end
end

#write(offset, value) ⇒ Object



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/psx/dma.rb', line 135

def write(offset, value)
  if offset < 0x70
    # Channel registers
    channel_num = offset >> 4
    reg = offset & 0xF

    return if channel_num >= NUM_CHANNELS

    channel = @channels[channel_num]
    case reg
    when 0x0
      channel.base_addr = value & 0x00FF_FFFC  # Word-aligned, 24-bit
    when 0x4
      channel.block_ctrl = value
    when 0x8
      # OTC (channel 6) has hard-wired CHCR bits: only bits 24, 28, 30
      # are writable, and bit 1 (Memory Address Step = backward) always
      # reads as 1. Verified against ps1-tests/dma/otc-test.
      if channel_num == OTC
        channel.channel_ctrl = (value & 0x5100_0000) | 0x0000_0002
      else
        channel.channel_ctrl = value
      end
    end
  elsif offset == 0x70
    @dpcr = value
  elsif offset == 0x74
    write_dicr(value)
  end
end

#write_dicr(value) ⇒ Object



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/psx/dma.rb', line 166

def write_dicr(value)
  # Bits 0-5: Unknown/unused
  # Bit 15: Force IRQ
  # Bits 16-22: IRQ enable for channels 0-6
  # Bit 23: Master IRQ enable
  # Bits 24-30: IRQ flags for channels 0-6 (write 1 to acknowledge)
  # Bit 31: Master IRQ flag (read-only)

  # Acknowledge flags by writing 1
  ack = value & DICR_IRQ_FLAGS
  @dicr &= ~ack

  # Update writable bits (preserve flags that weren't acknowledged)
  @dicr = (@dicr & DICR_IRQ_FLAGS) |
          (value & 0x00FF_803F)

  # Update master flag
  update_master_flag
end