Skip to content

Commit

Permalink
first draft at audio-in-hdmi
Browse files Browse the repository at this point in the history
  • Loading branch information
rdolbeau committed Jan 8, 2023
1 parent 59e3973 commit c18819f
Show file tree
Hide file tree
Showing 2 changed files with 212 additions and 15 deletions.
188 changes: 188 additions & 0 deletions goblin_alt_audio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
from migen import *
from migen.genlib.fifo import *
from litex.soc.interconnect.csr import *
from litex.soc.interconnect import wishbone

class GoblinAudio(Module, AutoCSR):
def __init__(self, soc, audio_clk_freq):

self.hdmiext_audio_clk = hdmiext_audio_clk = Signal() # should be audio_clk_freq/44.1 kHz clock
self.hdmiext_audio_word_0 = hdmiext_audio_word_0 = Signal(16) # channel 0 of stereo audio (L-PCM)
self.hdmiext_audio_word_1 = hdmiext_audio_word_1 = Signal(16) # channel 1 of stereo audio (L-PCM)

self.irq = Signal() # active LOW

self.busmaster = busmaster = wishbone.Interface()

# # #


self.irqctrl = CSRStorage(write_from_dev=True, fields = [CSRField("irq_enable", 1, description = "Enable interrupt"),
CSRField("irq_clear", 1, description = "Clear interrupt"),
CSRField("reserved", 30, description = "Reserved"),
])
self.irqstatus = CSRStatus(fields = [ CSRField("irq", 1, description = "There's a pending interrupt"),
CSRField("reserved", 31, description = "Reserved")
])


self.ctrl = CSRStorage(write_from_dev=True, fields = [CSRField("buffer_num", 1, description = "Buffer to use"),
CSRField("reserved0", 7, description = "Reserved"),
CSRField("play", 1, description = "Play (start with specified buffer and keel looping over all buffers)"),
CSRField("reserved1", 7, description = "Reserved"),
CSRField("autostop", 1, description = "Autostop when currently playing buffer is empty"),
CSRField("reserved2", 7, description = "Reserved"),
CSRField("reserved3", 8, description = "Reserved"),
])
self.bufstatus = CSRStatus(fields = [CSRField("buffer_num", 1, description = "Buffer in use"),
CSRField("reserved0", 7, description = "Reserved"),
CSRField("play", 1, description = "Playing"),
CSRField("reserved1", 7, description = "Reserved"),
CSRField("buffer_left", 16, description = "samples left"),
])
self.buf0_addr = CSRStorage(32, description = "Wishbone base address for audio buffer 0")
self.buf0_size = CSRStorage(fields = [CSRField("size", 16, description = "Number of samples in audio buffer 0"),
CSRField("reserved", 16, description = "Reserved"),
])
self.buf1_addr = CSRStorage(32, description = "Wishbone base address for audio buffer 1")
self.buf1_size = CSRStorage(fields = [CSRField("size", 16, description = "Number of samples in audio buffer 1"),
CSRField("reserved", 16, description = "Reserved"),
])

# handle IRQ
self.sync += If(self.irqctrl.fields.irq_clear, ## auto-reset irq_clear
self.irqctrl.we.eq(1),
self.irqctrl.dat_w.eq(self.irqctrl.storage & 0xFFFFFFFD)).Else(
self.irqctrl.we.eq(0),
)
temp_irq = Signal() # long internal irq (can be masked)
set_irq = Signal() # short transient irq (1 cycle ping from FSM)
self.sync += temp_irq.eq(set_irq | # transient irq signal
(temp_irq & ~self.irqctrl.fields.irq_clear)) # keep irq until cleared
self.comb += self.irq.eq(~(temp_irq & self.irqctrl.fields.irq_enable)) # only notify irq to the host if not disabled # self.irq active low
self.comb += self.irqstatus.fields.irq.eq(~self.irq) # self.irq active low

# basic 44.1 KHz clock based on system clock
audio_max = int((soc.sys_clk_freq / audio_clk_freq) + 0.5)
audio_max_bits = log2_int(audio_max, False)
audio_counter = Signal(audio_max_bits)
self.sync += [
If((audio_counter == (audio_max - 1)),
hdmiext_audio_clk.eq(1),
audio_counter.eq(0),
).Else(
hdmiext_audio_clk.eq(0),
audio_counter.eq(audio_counter + 1),
),
]

next_adr = Signal(30) # wishbone!
sample_cnt = Signal(16)
cur_buf = Signal(1)
next_buf = Signal(1)

self.comb += [
self.bufstatus.fields.buffer_num.eq(cur_buf),
self.bufstatus.fields.buffer_left.eq(sample_cnt),
]

need_data = Signal()
# self-reset need_data, whenever we consume a sample in hdmi (audio_clk) we request more
self.sync += [
If(~need_data & hdmiext_audio_clk,
need_data.eq(1),
)
]
# autostop
self.sync += [
If((sample_cnt == 0) & self.ctrl.fields.autostop & hdmiext_audio_clk, # we just consumed the last sample in autostop mode
self.ctrl.we.eq(1),
self.ctrl.dat_w.eq(self.ctrl.storage & 0xFFFFFEFF), # reset 'play'
).Else(
self.ctrl.we.eq(0),
),
]

self.submodules.play_fsm = play_fsm = FSM(reset_state="Reset")
play_fsm.act("Reset",
NextState("Silent")
)
play_fsm.act("Silent",
NextValue(hdmiext_audio_word_0, 0),
NextValue(hdmiext_audio_word_1, 0),
If(self.ctrl.fields.play,
NextValue(cur_buf, self.ctrl.fields.buffer_num),
NextValue(next_buf, self.ctrl.fields.buffer_num + 1),
NextValue(need_data, 1),
Case(self.ctrl.fields.buffer_num, {
0x0: [
NextValue(next_adr, self.buf0_addr.storage[2:32]),
NextValue(sample_cnt, self.buf0_size.fields.size),
],
0x1: [
NextValue(next_adr, self.buf1_addr.storage[2:32]),
NextValue(sample_cnt, self.buf1_size.fields.size),
],
}),
NextState("Play"),
)
)
play_fsm.act("Play",
If(~self.ctrl.fields.play,
NextValue(hdmiext_audio_word_0, 0),
NextValue(hdmiext_audio_word_1, 0),
NextState("Silent"),
).Elif((sample_cnt == 0), # ran out of sample in this buffer
set_irq.eq(self.irqctrl.fields.irq_enable), # transient notify to the host we emptied a buffer # only raised when enabled
If(~self.ctrl.fields.autostop,
NextValue(cur_buf, next_buf),
NextValue(next_buf, next_buf + 1),
Case(next_buf, {
0x0: [
NextValue(next_adr, self.buf0_addr.storage[2:32]),
NextValue(sample_cnt, self.buf0_size.fields.size),
],
0x1: [
NextValue(next_adr, self.buf1_addr.storage[2:32]),
NextValue(sample_cnt, self.buf1_size.fields.size),
],
}),
)
# stay in "Play"
)
)

led0 = soc.platform.request("user_led", 0)
self.comb += [
self.bufstatus.fields.play.eq(play_fsm.ongoing("Play")),
led0.eq(play_fsm.ongoing("Play")),
]

# auto-reload

self.submodules.req_fsm = req_fsm = FSM(reset_state="Reset")
req_fsm.act("Reset",
NextState("Idle")
)
req_fsm.act("Idle",
If(need_data & self.ctrl.fields.play & play_fsm.ongoing("Play") & (sample_cnt != 0),
NextValue(busmaster.cyc, 1),
NextValue(busmaster.stb, 1),
NextValue(busmaster.sel, 2**len(busmaster.sel)-1),
NextValue(busmaster.we, 0),
NextValue(busmaster.adr, next_adr),
NextState("WaitForAck")
)
)
req_fsm.act("WaitForAck",
If(busmaster.ack,
NextValue(busmaster.cyc, 0),
NextValue(busmaster.stb, 0),
NextValue(hdmiext_audio_word_0, Cat(busmaster.dat_r[ 8:16], busmaster.dat_r[ 0: 8])), # fixme: endianess
NextValue(hdmiext_audio_word_1, Cat(busmaster.dat_r[24:32], busmaster.dat_r[16:24])), # fixme: endianess
NextValue(next_adr, next_adr + 1), # fixme
NextValue(sample_cnt, sample_cnt - 1),
NextValue(need_data, 0), # this will self-reset to 1 above after the next audio_clk cycle, i.e. when hdmi consume the data
NextState("Idle"),
)
)
39 changes: 24 additions & 15 deletions goblin_alt_fb.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from VintageBusFPGA_Common.fb_video import *
from VintageBusFPGA_Common.fb_dma import LiteDRAMFBDMAReader

from VintageBusFPGA_Common.goblin_alt_audio import GoblinAudio

from math import ceil

cmap_layout = [
Expand Down Expand Up @@ -674,6 +676,13 @@ def __init__(self, soc=None, timings=None, clock_domain="sys", irq_line=None, en
hdmiext_rgb = Signal(24) # three colors at 8 bits each, fixme
hdmiext_audio_word_0 = Signal(16) # channel 0 of stereo audio, fixme
hdmiext_audio_word_1 = Signal(16) # channel 1 of stereo audio, fixme

self.submodules.goblin_audio = GoblinAudio(soc, 44.1e3)
self.comb += [
hdmiext_audio_clk.eq(self.goblin_audio.hdmiext_audio_clk),
hdmiext_audio_word_0.eq(self.goblin_audio.hdmiext_audio_word_0),
hdmiext_audio_word_1.eq(self.goblin_audio.hdmiext_audio_word_1),
]

## OUTPUTS from the HDMI module
hdmiext_tmds = Signal(3) # high-speed colors to the TMDS bits
Expand All @@ -693,8 +702,8 @@ def __init__(self, soc=None, timings=None, clock_domain="sys", irq_line=None, en
hdmiext_rgb[ 0: 8].eq(vfb.source.b),
hdmiext_rgb[ 8:16].eq(vfb.source.g),
hdmiext_rgb[16:24].eq(vfb.source.r),
hdmiext_audio_word_0.eq(0), # fixme: implement
hdmiext_audio_word_1.eq(0), # fixme: implement
## hdmiext_audio_word_0.eq(0), # fixme: implement
## hdmiext_audio_word_1.eq(0), # fixme: implement
# use VTG enable to generate reset, to we have the same relationship to the DMA
hdmiext_reset.eq(~vtg_enable),
]
Expand Down Expand Up @@ -757,19 +766,19 @@ def __init__(self, soc=None, timings=None, clock_domain="sys", irq_line=None, en
vfb.inframe.eq(hinframe & vinframe),
]

# basic 44.1 KHz clock based on the HDMI clock
audio_max = int((vtg.video_timings["pix_clk"] / 44100.0) + 0.5)
audio_max_bits = log2_int(audio_max, False)
audio_counter = Signal(audio_max_bits)
hdmi_sync += [
If(audio_counter == (audio_max - 1),
hdmiext_audio_clk.eq(1),
audio_counter.eq(0),
).Else(
hdmiext_audio_clk.eq(0),
audio_counter.eq(audio_counter + 1),
),
]
### basic 44.1 KHz clock based on the HDMI clock
##audio_max = int((vtg.video_timings["pix_clk"] / 44100.0) + 0.5)
##audio_max_bits = log2_int(audio_max, False)
##audio_counter = Signal(audio_max_bits)
##hdmi_sync += [
## If(audio_counter == (audio_max - 1),
## hdmiext_audio_clk.eq(1),
## audio_counter.eq(0),
## ).Else(
## hdmiext_audio_clk.eq(0),
## audio_counter.eq(audio_counter + 1),
## ),
##]

self.specials += Instance("hdmi",
p_VIDEO_ID_CODE = 16, # CEA-861-D, "4.15 1920x1080p @ 59.94/60Hz (Format 16)", FIXME
Expand Down

0 comments on commit c18819f

Please sign in to comment.