diff --git a/examples/mega65/CMakeLists.txt b/examples/mega65/CMakeLists.txt index 60c6a2c0..a58e6fe0 100644 --- a/examples/mega65/CMakeLists.txt +++ b/examples/mega65/CMakeLists.txt @@ -4,3 +4,13 @@ install_example(viciv_test.prg) add_executable(plasma.prg plasma.cc) install_example(plasma.prg) +add_executable(vertical_raster.prg vertical_raster.cc) +install_example(vertical_raster.prg) + +add_executable(simple_dma.prg simple_dma.cc) +install_example(simple_dma.prg) + +add_executable(dma_audio.prg dma_audio.c) +install_example(dma_audio.prg) +install(FILES drums.s8 TYPE BIN) + diff --git a/examples/mega65/dma_audio.c b/examples/mega65/dma_audio.c new file mode 100644 index 00000000..02dd726b --- /dev/null +++ b/examples/mega65/dma_audio.c @@ -0,0 +1,34 @@ +// Copyright 2024 LLVM-MOS Project +// Licensed under the Apache License, Version 2.0 with LLVM Exceptions. +// See https://github.com/llvm-mos/llvm-mos-sdk/blob/main/LICENSE for license +// information. + +// MEGA65 DMA Audio example using C23 + +#include + +typedef unsigned _BitInt(24) uint24_t; + +// Sample data prepared with: +// ~~~ +// sox -v 2.0 -V -D drums.wav drums.s8 remix - lowpass 7000 rate -v -s -I 11822 dither -S +// ~~~ +static const uint8_t sample[] = { +#embed "drums.s8" +}; + +int main(void) { + DMA.auden = DMA_AUDEN; // enable DMA audio + DMA.ch0rvol = 0; // mute right + DMA.ch0.enable = 0; // mute channel 0 + DMA.ch0.baddr = (uint24_t)&sample; // base address + DMA.ch0.curaddr = (uint24_t)&sample; // current address + DMA.ch0.taddr = (uint16_t)&sample + sizeof(sample); // top address + DMA.ch0.freq = 0x001a88; // frequency + DMA.ch0.volume = 0x3f; // max volume + DMA.ch0.enable = DMA_CHENABLE ^ DMA_CHSBITS_8 ^ DMA_CHLOOP; // play! + while (1) { + // Leaves at least one cycle for DMA audio to steal + VICIV.bordercol += 1; + } +} diff --git a/examples/mega65/drums.license b/examples/mega65/drums.license new file mode 100644 index 00000000..169135ce --- /dev/null +++ b/examples/mega65/drums.license @@ -0,0 +1,4 @@ +Source: https://commons.wikimedia.org/wiki/File:Patró_de_bateria.wav +Attribution: Escola Superior de Música de Catalunya (ESMUC) +License: CC BY-SA 3.0 , via Wikimedia Commons +Note: Modified from original. diff --git a/examples/mega65/drums.s8 b/examples/mega65/drums.s8 new file mode 100644 index 00000000..8a526eed Binary files /dev/null and b/examples/mega65/drums.s8 differ diff --git a/examples/mega65/simple_dma.cc b/examples/mega65/simple_dma.cc new file mode 100644 index 00000000..dc93298f --- /dev/null +++ b/examples/mega65/simple_dma.cc @@ -0,0 +1,26 @@ +// Copyright 2024 LLVM-MOS Project +// Licensed under the Apache License, Version 2.0 with LLVM Exceptions. +// See https://github.com/llvm-mos/llvm-mos-sdk/blob/main/LICENSE for license +// information. + +#include +#include + +using namespace mega65::dma; + +constexpr uint32_t SCREEN_ADDR = 0x0800; // Screen area +constexpr uint16_t COUNT = 4; // Bytes to fill +constexpr uint8_t CHAR = 41; // Char symbol to print + +int main(void) { + { + // repeat some chars on first line + const auto dma = make_dma_fill(SCREEN_ADDR, CHAR, COUNT); + trigger_dma(dma); + } + { + // copy chars from above to second line + const auto dma = make_dma_copy(SCREEN_ADDR, SCREEN_ADDR + 80, COUNT); + trigger_dma(dma); + } +} diff --git a/examples/mega65/vertical_raster.cc b/examples/mega65/vertical_raster.cc new file mode 100644 index 00000000..652dff9a --- /dev/null +++ b/examples/mega65/vertical_raster.cc @@ -0,0 +1,88 @@ +// Copyright 2024 LLVM-MOS Project +// Licensed under the Apache License, Version 2.0 with LLVM Exceptions. +// See https://github.com/llvm-mos/llvm-mos-sdk/blob/main/LICENSE for license +// information. + +// Vertical Raster Bars using the DMAgic DMA controller. +// +// - Timing is crucial: specify NTSC or PAL below. +// - Compile with `mos-mega65-clang++ -Os -flto` +// - Originally from ACME assembler example in the MEGA65 book +// - Converted to llvm-mos / C++ by Wombat, 2024. +// - As of writing (Summer 2024) this runs only on real hardware + +#include +#include + +using namespace mega65; + +constexpr bool PAL = true; // Set to false on NTSC systems + +constexpr uint8_t RASTER_COLORS[] = { + 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4, + 5, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, + 3, 4, 5, 6, 7, 8, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, + 3, 4, 5, 6, 7, 8, 9, 10, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + 11, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 0, 1, 2, 3, 4, + 5, 6, 7, 8, 9, 10, 11, 12, 13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, + 13, 14, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 14, + 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 13, 12, 11, 10, 9, + 8, 7, 6, 5, 4, 3, 2, 1, 0, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, + 2, 1, 0, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 10, 9, 8, 7, + 6, 5, 4, 3, 2, 1, 0, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 8, 7, + 6, 5, 4, 3, 2, 1, 0, 7, 6, 5, 4, 3, 2, 1, 0, 6, 5, 4, 3, + 2, 1, 0, 5, 4, 3, 2, 1, 0, 4, 3, 2, 1, 0, 3, 2, 1, 0, 2, + 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, + 4, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, + 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 1, 2, 3, 4, 5, 6, + 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0, 1, 2, 3, 4, + 5, 6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, + 12, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 0, 1, 2, 3, + 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 0, 0, 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, + 4, 3, 2, 1, 0, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, + 0, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 12, 11, 10, 9, + 8, 7, 6, 5, 4, 3, 2, 1, 0, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, + 1, 0, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 9, 8, 7, 6, 5, 4, + 3, 2, 1, 0, 8, 7, 6, 5, 4, 3, 2, 1, 0, 7, 6, 5, 4, 3, 2, + 1, 0, 6, 5, 4, 3, 2, 1, 0, 5, 4, 3, 2, 1, 0, 4, 3, 2, 1, + 0, 3, 2, 1, 0, 2, 1, 0, 1, 0, 0, 0, +}; + +int main() { + asm volatile("sei"); // Disable interrupt to stabilise timing + VICIV.ctrl1 = 0; // Disable screen + + // Set custom color palette + for (size_t i = 1; i < 16; i++) { + PALETTE.red[i] = 15 - i; + PALETTE.green[i] = 0; + PALETTE.blue[i] = i; + } + + // Configure DMA job; see The MEGA65 Book, Appendix O. + dma::DMAJob<2, DMAList_F018A> dma; + dma.options[0] = DST_ADDR_BITS_OPT; + dma.options[1] = 0xff; + dma.dmalist.command = DMA_COPY_CMD; + dma.dmalist.count = PAL ? 628 : 624; + dma.dmalist.source_addr = (uint16_t)&RASTER_COLORS; + dma.dmalist.source_bank = 0x00; + dma.dmalist.dest_addr = 0x0020; + dma.dmalist.dest_bank = 0x0d ^ DMA_HOLD; + dma.dmalist.modulo = 0; + + // Wait for new raster line + const auto line = VICIV.rasterline; + while (line == VICIV.rasterline) + ; + +#pragma clang loop unroll(disable) + while (true) { + dma::trigger_dma(dma); + if constexpr (PAL) { + asm volatile("nop"); + } + } +} diff --git a/mos-platform/mega65/CMakeLists.txt b/mos-platform/mega65/CMakeLists.txt index 37dfeaf7..63bbd0a8 100644 --- a/mos-platform/mega65/CMakeLists.txt +++ b/mos-platform/mega65/CMakeLists.txt @@ -5,8 +5,10 @@ if(NOT CMAKE_CROSSCOMPILING) endif() install(FILES _45E100.h + _dmagic.h _vic3.h _vic4.h + dma.hpp mega65.h TYPE INCLUDE) diff --git a/mos-platform/mega65/_dmagic.h b/mos-platform/mega65/_dmagic.h new file mode 100644 index 00000000..06fbafb7 --- /dev/null +++ b/mos-platform/mega65/_dmagic.h @@ -0,0 +1,213 @@ +// Copyright 2023 LLVM-MOS Project +// Licensed under the Apache License, Version 2.0 with LLVM Exceptions. +// See https://github.com/llvm-mos/llvm-mos-sdk/blob/main/LICENSE for license +// information. + +#ifndef _DMAGIC_H +#define _DMAGIC_H + +#ifndef __cplusplus +#include +#include +#endif + +/// DMA commands +enum +#ifdef __clang__ + : uint8_t +#endif +{ + DMA_COPY_CMD = 0x00, //!< DMA copy command + DMA_MIX_CMD = 0x01, //!< DMA mix command (unimplemented) + DMA_SWAP_CMD = 0x02, //!< DMA swap command (unimplemented) + DMA_FILL_CMD = 0x03, //!< DMA fill command +}; + +/// Addressing modes +enum +#ifdef __clang__ + : uint8_t +#endif +{ + DMA_LINEAR_ADDR = 0x00, //!< DMA linear (normal) addressing mode + DMA_MODULO_ADDR = 0x01, //!< DMA modulo (rectangular) addressing mode + DMA_HOLD_ADDR = 0x02, //!< DMA hold (constant address) addressing mode + DMA_XYMOD_ADDR = + 0x03 //!< DMA XY MOD (bitmap rectangular) addressing mode (unimplemented) +}; + +/// BANK and FLAGS field has the following structure +enum +#ifdef __clang__ + : uint8_t +#endif +{ + DMA_HOLD = 16, //!< Do not change the address (bit 4) + DMA_MODULO = 32, //!< Apply the MODULO field to wrap around (bit 5) + DMA_DIRECTION = 64, //!< Apply the MODULO field to wrap around (bit 6) + DMA_IO = 128, //!< I/O registers visible during the DMA controller at $D000–$DFFF (bit 7). +}; + +/// DMA options (incomplete) +enum +#ifdef __clang__ + : uint8_t +#endif +{ + /// Use 11 byte F011A DMA list format [no value] + ENABLE_F018A_OPT = 0x0a, + /// Use 12 byte F011B DMA list format [no value] + ENABLE_F018B_OPT = 0x0b, + /// Source address bits 20 – 27 [value follows] + SRC_ADDR_BITS_OPT = 0x80, + /// Destination address bits 20 – 27 [value follows] + DST_ADDR_BITS_OPT = 0x81, + /// Destination skip rate (whole bytes) [value follows] + DST_SKIP_RATE_OPT = 0x85, +}; + +/// DMA audio channel structure +struct DMAAudioChannel { + uint8_t enable; //!< Enable Audio DMA channel X (offset 0x00) + union { + struct { + uint8_t baddr_lsb; //!< Base address LSB (offset 0x01) + uint8_t baddr_msb; //!< Base address MSB (offset 0x02) + uint8_t baddr_mb; //!< Base address middle byte (offset 0x03) + }; +#ifdef __clang__ + unsigned _BitInt(24) baddr; //!< 24-bit base address (offset 0x01) +#endif + }; + union { + struct { + uint8_t freq_lsb; //!< Frequency LSB (offset 0x04) + uint8_t freq_mb; //!< Frequency middle byte (offset 0x05) + uint8_t freq_msb; //!< Frequency MSB (offset 0x06) + }; +#ifdef __clang__ + unsigned _BitInt(24) freq; //!< 24-bit frequency (offset 0x04) +#endif + }; + union { + struct { + uint8_t taddr_lsb; //!< Top address LSB (offset 0x07) + uint8_t taddr_msb; //!< Top address MSB (offset 0x08) + }; +#ifdef __clang__ + unsigned _BitInt(16) taddr; //!< 16-bit top address (offset 0x07) +#endif + }; + uint8_t volume; //!< playback volume (offset 0x09) + union { + struct { + uint8_t curaddr_lsb; //!< Current address LSB (offset 0x0a) + uint8_t curaddr_mb; //!< Current address middle byte (offset 0x0b) + uint8_t curaddr_msb; //!< Current address MSB (offset 0x0c) + }; +#ifdef __clang__ + unsigned _BitInt(24) curaddr; //!< 24-bit current address (offset 0x0a) +#endif + }; + union { + struct { + uint8_t tmraddr_lsb; //!< Timing counter LSB (offset 0x0d) + uint8_t tmraddr_mb; //!< Timing counter middle byte (offset 0x0e) + uint8_t tmraddr_msb; //!< Timing counter MSB (offset 0x0f) + }; +#ifdef __clang__ + unsigned _BitInt(24) tmraddr; //!< 24-bit timing counter (offset 0x0d) +#endif + }; +}; + +#ifdef __cplusplus +static_assert(sizeof(DMAAudioChannel) == 0x10); +#endif + +/// Bitflags for controlling the DMA audio enable register ($d711) +enum +#ifdef __clang__ + : uint8_t +#endif +{ + DMA_AUDEN = 0b10000000, //!< Enable Audio DMA + DMA_BLKD = 0b01000000, //!< Block DMA + DMA_AUD_WRBLK = 0b00100000, //!< Audio write block + DMA_NOMIX = 0b00010000, //!< No mix; disables SID chips etc. + DMA_AUDBLKTO = 0b00000111, //!< Audio block timeout (DEBUG) Bits 0-2 +}; + +/// Bitflags for controlling individual DMA audio channel enable registers +/// ($d720, etc) +enum +#ifdef __clang__ + : uint8_t +#endif +{ + DMA_CHENABLE = 0b10000000, //!< Enable channel + DMA_CHLOOP = 0b01000000, //!< Channel looping + DMA_CHSGN = 0b00100000, //!< Sample sign bit: 0=signed, 1=unsigned. + DMA_CHSINE = 0b00010000, //!< Play 32-sample sine wave instead of DMA data + DMA_CHSTP = 0b00001000, //!< Stop flag + DMA_CHSBITS_16 = 0b00000011, //!< 16-bit samples + DMA_CHSBITS_8 = 0b00000010, //!< 8-bit samples +}; + +/// The F018 "DMAgic" DMA controller at 0xd700 +struct DMAgicController { + uint8_t addr_lsb_trigger; //!< DMAgic DMA list address LSB, and trigger DMA (when written) (offset 0x00) + uint8_t addr_msb; //!< DMA list address high byte (address bits 8 – 15) (offset 0x01) + uint8_t addr_bank; //!< DMA list address bank (address bits 16 – 22). Writing clears $D704 (offset 0x02) + uint8_t enable_f018b; //!< Offset 0x03, extensed fields + uint8_t addr_mb; //!< DMA list address mega-byte (offset 0x04) + uint8_t trigger_enhanced; //!< Set low-order byte of DMA list address, and trigger Enhanced DMA job (offset 0x05) + uint8_t etrigmapd; //!< Set low-order byte of DMAlistaddress and trigger EnhancedDMA job, with list in current CPU memory map (offset 0x06) + uint8_t unused1[7]; //!< Offset 0x07-0x0d + uint8_t addr_lsb; //!< DMA list address low byte (address bits 0 – 7) WITHOUT STARTING A DMA JOB (offset 0x0e) + uint8_t unused2[2]; //!< Offset 0x0f-0x10 + uint8_t auden; //!< Enable Audio DMA (offset 0x11) + uint8_t unused3[10]; //!< Offset 0x12-0x1b + uint8_t ch0rvol; //!< Channel 0 right channel volume (offset 0x1c) + uint8_t ch1rvol; //!< Channel 1 right channel volume (offset 0x1d) + uint8_t ch2lvol; //!< Channel 2 left channel volume (offset 0x1e) + uint8_t ch3lvol; //!< Channel 3 left channel volume (offset 0x1f) + union { + struct { + struct DMAAudioChannel ch0; //!< Audio DMA channel 0 (offset 0x20) + struct DMAAudioChannel ch1; //!< Audio DMA channel 1 (offset 0x30) + struct DMAAudioChannel ch2; //!< Audio DMA channel 2 (offset 0x40) + struct DMAAudioChannel ch3; //!< Audio DMA channel 3 (offset 0x50) + }; + struct DMAAudioChannel channel[4]; //!< Audio channels as an array (offset 0x20) + }; +}; + +#ifdef __cplusplus +static_assert(sizeof(DMAgicController) == 0x60); +#endif + +/// Older 11 byte DMA list structure; also known as just "F018" +struct DMAList_F018A { + uint8_t command; //!< Offset 0x00 + uint16_t count; //!< Offset 0x01 + uint16_t source_addr; //!< Offset 0x03 + uint8_t source_bank; //!< Offset 0x05 + uint16_t dest_addr; //!< Offset 0x06 + uint8_t dest_bank; //!< Offset 0x08 + uint16_t modulo; //!< Offset 0x09 +}; + +/// Newer 12-byte "F018B" DMA list structure +struct DMAList_F018B { + uint8_t command; //!< Offset 0x00 + uint16_t count; //!< Offset 0x01 + uint16_t source_addr; //!< Offset 0x03 + uint8_t source_bank; //!< Offset 0x05 + uint16_t dest_addr; //!< Offset 0x06 + uint8_t dest_bank; //!< Offset 0x08 + uint8_t command_msb; //!< Offset 0x09 + uint16_t modulo; //!< Offset 0x0a +}; + +#endif diff --git a/mos-platform/mega65/dma.hpp b/mos-platform/mega65/dma.hpp new file mode 100644 index 00000000..f976aecc --- /dev/null +++ b/mos-platform/mega65/dma.hpp @@ -0,0 +1,105 @@ +// Copyright 2023 LLVM-MOS Project +// Licensed under the Apache License, Version 2.0 with LLVM Exceptions. +// See https://github.com/llvm-mos/llvm-mos-sdk/blob/main/LICENSE for license +// information. +// +// C++ support for MEGA65 DMA operations. +// +// +#ifndef _MEGA65_DMA_HPP +#define _MEGA65_DMA_HPP + +#include +#include +#include +#include + +namespace mega65::dma { + +/** + * Data structure for running enhanced DMA jobs + * + * @tparam T Either a F018B (default) or F018A DMA list object. + * @tparam N Byte count of the options and their potential + * arguments excluding the end option (0) which is automatically + * added. + */ +template struct DMAJob { + static_assert(N > 0); + static_assert(std::is_same::value || + std::is_same::value); + std::array options; + const uint8_t end_option = 0; + T dmalist; +}; + +/// Common structure for DMA fill and copy jobs +typedef DMAJob<7, DMAList_F018B> CommonDMAJob; + +/** + * Create DMA structure for filling memory with a value and optional skip + * + * To perform the actual fill, call `trigger_dma()` on the returned structure. + * + * @param dst 28-bit destination address + * @param value Fill value + * @param count Number of values to fill + * @param skip Optional skip (default: 1) + */ +CommonDMAJob make_dma_fill(const uint32_t dst, const uint8_t value, + const uint16_t count, const uint8_t skip = 1) { + CommonDMAJob dma; + dma.options[0] = ENABLE_F018B_OPT; + dma.options[1] = SRC_ADDR_BITS_OPT; + dma.options[2] = 0; + dma.options[3] = DST_ADDR_BITS_OPT; + dma.options[4] = (uint8_t)(dst >> 20); + dma.options[5] = DST_SKIP_RATE_OPT; + dma.options[6] = skip; + dma.dmalist.command = DMA_FILL_CMD; + dma.dmalist.count = count; + dma.dmalist.source_addr = value; + dma.dmalist.source_bank = 0; + dma.dmalist.dest_addr = dst & 0xffff; + dma.dmalist.dest_bank = (dst >> 16) & 0x0f; + dma.dmalist.command_msb = 0; + dma.dmalist.modulo = 0; + return dma; +} + +/** + * Create DMA structure for copying memory + * + * To perform the actual copy, call `trigger_dma()` on the returned structure. + * + * @param src 28-bit source address + * @param dst 28-bit destination address + * @param count Number of values to copy + */ +CommonDMAJob make_dma_copy(const uint32_t src, const uint32_t dst, + const uint16_t count) { + auto dma = make_dma_fill(dst, 0, count); + dma.options[2] = (uint8_t)(src >> 20); + dma.dmalist.command = DMA_COPY_CMD; + dma.dmalist.source_addr = src & 0xffff; + dma.dmalist.source_bank = (src >> 16) & 0x0f; + return dma; +} + +/** + * Perform enhanced DMA action defined in DMAJob structure. + */ +template +inline void trigger_dma(const DMAJob &dma_job) { + DMA.enable_f018b = std::is_same::value; + DMA.addr_bank = 0; + DMA.addr_msb = ((uint16_t)&dma_job) >> 8; + DMA.trigger_enhanced = ((uint16_t)&dma_job) & 0xff; + // Avoid the above from being optimized out. Ideally + // we would want to somehow access `dma_job`, but an + // empty statement seems to be sufficient. + asm volatile(""); +} + +} // namespace mega65::dma +#endif // _MEGA65_DMA_HPP diff --git a/mos-platform/mega65/mega65.h b/mos-platform/mega65/mega65.h index aaf48f20..025d6847 100644 --- a/mos-platform/mega65/mega65.h +++ b/mos-platform/mega65/mega65.h @@ -22,6 +22,7 @@ extern "C" { #include <_45E100.h> #include <_6526.h> +#include <_dmagic.h> #include <_sid.h> #include <_vic2.h> #include <_vic3.h> @@ -162,6 +163,8 @@ struct __color_palette { #define HYPERVISOR (*(volatile struct __hypervisor *)0xd640) /// Ethernet controller #define ETHERNET (*(volatile struct __45E100 *)0xd6e0) +/// DMAgic DMA controller +#define DMA (*(volatile struct DMAgicController *)0xd700) /// Math busy flag #define MATHBUSY (*(volatile uint8_t *)0xd70f) /// Math accelerator