opl: enable EMU8950 TLL via pre-computed table in flash (.flashdata section)

The TLL (Total Level Lookup) table is 128KB — too large for the RP2040's
256KB RAM when the firmware also runs code from RAM. Previous attempts to
use a `const` array failed because the firmware uses a copy_to_ram binary
type, which copies ALL sections including `.rodata` into RAM at boot.

The fix: place the pre-computed table in the `.flashdata.tll_table` linker
section. Both pico-sdk's `memmap_copy_to_ram.ld` and the local
`fw_memmap_c2ram.ld` explicitly capture `.flashdata*` into the FLASH
`.rodata` region, which is accessed via XIP cache and never copied to RAM.

- gen_tll_table.py: new script that pre-computes makeTllTable() using the
  EMU8950_NO_FLOAT integer path, outputting tll_table_flash.c with the
  `.flashdata.tll_table` section attribute
- tll_table_flash.c: generated 128KB table (32,768 uint32_t values)
- emu8950.c: support EMU8950_TLL_FLASH — use extern const declaration and
  skip makeTllTable() at runtime when table is pre-computed in flash
- CMakeLists: replace EMU8950_NO_TLL=1 with EMU8950_TLL_FLASH=1

TLL improves OPL2 accuracy by enabling the full Key Scale Level (KSL)
attenuation lookup table rather than the inline approximation.
This commit is contained in:
Daniel Arnold authored and Ian Scott committed 2026-03-28 11:53:01 -06:00
1 parent 1ceb476dfd
commit f3220f736a
5 files changed
+8539 -3

No files matched your search

+2 -2
View File
@@ -170,8 +170,8 @@ function(config_target TARGET_NAME MULTIFW)
SOUND_OPL=1
USE_EMU8950_OPL=1
EMU8950_ASM=1
EMU8950_NO_TLL=1 # don't use lookup table for total level
EMU8950_NO_FLOAT=1 # double check there is no float
EMU8950_TLL_FLASH=1 # pre-computed TLL table stored in flash via .flashdata section (not copied to RAM)
EMU8950_NO_FLOAT=1 # RP2040 has no FPU; float only used in TLL init anyway
EMU8950_NO_TIMER=1 # disable timer which isn't used
EMU8950_NO_TEST_FLAG=1 # disable test flags (which aren't used)
)
+1
View File
@@ -2,6 +2,7 @@ add_library(opl INTERFACE)
target_sources(opl INTERFACE
# ${CMAKE_CURRENT_LIST_DIR}/opl_api.c
${CMAKE_CURRENT_LIST_DIR}/emu8950.c
${CMAKE_CURRENT_LIST_DIR}/tll_table_flash.c
${CMAKE_CURRENT_LIST_DIR}/slot_render.cpp
${CMAKE_CURRENT_LIST_DIR}/opl_pico.c)
target_compile_options(opl INTERFACE -fms-extensions) # want OPL_SLOT_RENDER to be unnamed within OPL_SLOT
+5 -1
View File
@@ -200,8 +200,12 @@ static int16_t kl_tablex16[16] = {dB2x16(0.000), dB2x16(9.000), dB2x16(12.000),
#endif
#if !EMU8950_NO_TLL
#ifdef EMU8950_TLL_FLASH
extern const uint32_t tll_table[8 * 16][1 << TL_BITS][4];
#else
static uint32_t tll_table[8 * 16][1 << TL_BITS][4];
#endif
#endif
static int32_t rks_table[2][32][2];
#define min(i, j) (((i) < (j)) ? (i) : (j))
@@ -368,7 +372,7 @@ static void makeSinTable(void) {
}
static void makeTllTable(void) {
#if !EMU8950_NO_TLL
#if !EMU8950_NO_TLL && !defined(EMU8950_TLL_FLASH)
int32_t tmp;
int32_t fnum, block, TL, KL, kx;
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""
Generates tll_table_flash.c for the EMU8950 OPL2 emulator.
Pre-computes makeTllTable() so the result can be stored in RP2040 flash (XIP)
instead of RAM, working around the 128KB RAM constraint.
Replicates the EMU8950_NO_FLOAT integer path from emu8950.c:
kl_tablex16[fnum] = (uint16_t)(kl_values[fnum] * 32)
dB2x16(3.000) = 96
EG_STEPx16 = 3
TL2EG(tl) = tl << 2
"""
TL_BITS = 6
EG_STEPx16 = 3
kl_values = [
0.000, 9.000, 12.000, 13.875, 15.000, 16.125,
16.875, 17.625, 18.000, 18.750, 19.125, 19.500,
19.875, 20.250, 20.625, 21.000,
]
kl_tablex16 = [int(v * 32) & 0xFFFF for v in kl_values] # uint16_t cast
dB2x16_3 = int(3.000 * 32) # = 96
def TL2EG(tl):
return tl << 2
# Build table[128][64][4]
table = [[[0] * 4 for _ in range(1 << TL_BITS)] for _ in range(8 * 16)]
for fnum in range(16):
for block in range(8):
idx = (block << 4) | fnum
for TL in range(1 << TL_BITS):
for KL in range(4):
if KL == 0:
table[idx][TL][KL] = TL2EG(TL)
else:
kx = ((KL & 1) << 1) | ((KL >> 1) & 1)
tmp = kl_tablex16[fnum] - dB2x16_3 * (7 - block)
if tmp <= 0:
table[idx][TL][KL] = TL2EG(TL)
else:
table[idx][TL][KL] = (tmp >> (3 - kx)) // EG_STEPx16 + TL2EG(TL)
lines = []
lines.append("/* Auto-generated by gen_tll_table.py — do not edit manually. */")
lines.append("/* Pre-computed EMU8950 TLL (Total Level Lookup) table for RP2040 flash (XIP). */")
lines.append("/* Replicates makeTllTable() integer path; stored as const in .flashdata section so")
lines.append(" * pico-sdk copy_to_ram linker scripts keep it in flash rather than copying to RAM. */")
lines.append("#include <stdint.h>")
lines.append("")
lines.append("/* __attribute__((section(\".flashdata.tll_table\"))) keeps this in flash even in")
lines.append(" * copy_to_ram builds (both memmap_copy_to_ram.ld and fw_memmap_c2ram.ld capture")
lines.append(" * .flashdata* into the FLASH .rodata region, not the RAM .data region). */")
lines.append("const uint32_t __attribute__((section(\".flashdata.tll_table\")))")
lines.append(" tll_table[128][64][4] = {")
for i in range(128):
lines.append(" { /* blk_fnum=" + str(i) + " */")
for j in range(64):
row = ", ".join(str(table[i][j][k]) for k in range(4))
lines.append(" {" + row + "},")
lines.append(" },")
lines.append("};")
import os
out_path = os.path.join(os.path.dirname(__file__), "tll_table_flash.c")
with open(out_path, "w") as f:
f.write("\n".join(lines) + "\n")
print(f"Written {len(table) * 64 * 4} uint32_t values to {out_path}")
File diff suppressed because it is too large. Load diff