1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-11-29 12:50:28 +00:00

Makes a first attempt at implementing the SN76489.

This commit is contained in:
Thomas Harte 2018-02-27 22:25:12 -05:00
parent 0ad2676640
commit d4df101ab6
4 changed files with 168 additions and 13 deletions

View File

@ -108,6 +108,7 @@ void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) {
}
evaluate_output_volume();
printf("%d ", output_volume_);
for(int ic = 0; ic < 8 && c < number_of_samples; ic++) {
target[c] = output_volume_;

View File

@ -8,9 +8,128 @@
#include "SN76489.hpp"
#include <cmath>
using namespace TI;
SN76489::SN76489(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) {}
void SN76489::write(uint8_t value) {
SN76489::SN76489(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) {
// Build a volume table.
double multiplier = pow(10.0, -0.1);
double volume = 8191.0f;
for(int c = 0; c < 16; ++c) {
volumes_[c] = (int)round(volume);
volume *= multiplier;
}
volumes_[15] = 0;
evaluate_output_volume();
}
void SN76489::set_register(uint8_t value) {
task_queue_.defer([value, this] () {
if(value & 0x80) {
active_register_ = value;
}
const int channel = (active_register_ >> 5)&3;
if(active_register_ & 0x10) {
// latch for volume
channels_[channel].volume = value & 0xf;
evaluate_output_volume();
} else {
// latch for tone/data
if(channel < 3) {
if(value & 0x80) {
channels_[channel].divider = (channels_[channel].divider & ~0xf) | (value & 0xf);
} else {
channels_[channel].divider = static_cast<uint16_t>((channels_[channel].divider & 0xf) | ((value & 0x3f) << 4));
}
} else {
// writes to the noise register always reset the shifter
noise_shifter_ = shifter_is_16bit_ ? 0x8000 : 0x4000;
if(value & 4) {
noise_mode_ = shifter_is_16bit_ ? Noise16 : Noise15;
} else {
noise_mode_ = shifter_is_16bit_ ? Periodic16 : Periodic15;
}
channels_[3].divider = static_cast<uint16_t>(0x10 << (value & 3));
// Special case: if these bits are both set, the noise channel should track channel 2,
// which is marked with a divider of 0xffff.
if(channels_[3].divider == 0x80) channels_[3].divider = 0xffff;
}
}
});
}
void SN76489::evaluate_output_volume() {
output_volume_ = static_cast<int16_t>(
channels_[0].level * volumes_[channels_[0].volume] +
channels_[1].level * volumes_[channels_[1].volume] +
channels_[2].level * volumes_[channels_[2].volume] +
channels_[3].level * volumes_[channels_[3].volume]
);
}
void SN76489::get_samples(std::size_t number_of_samples, std::int16_t *target) {
// For now: assume a divide by eight.
std::size_t c = 0;
while((master_divider_&7) && c < number_of_samples) {
target[c] = output_volume_;
master_divider_++;
c++;
}
while(c < number_of_samples) {
bool did_flip = false;
#define step_channel(c) \
if(channels_[c].counter) channels_[c].counter--;\
else {\
channels_[c].level ^= 1;\
channels_[c].counter = channels_[c].divider;\
did_flip = true;\
}
step_channel(0);
step_channel(1);
step_channel(2);
#undef step_channel
if(channels_[c].divider != 0xffff) {
if(channels_[3].counter) channels_[3].counter--;
else {
did_flip = true;
channels_[c].counter = channels_[c].divider;
}
}
if(did_flip) {
channels_[3].level = noise_shifter_ & 1;
int new_bit = channels_[3].level;
switch(noise_mode_) {
default: break;
case Noise15:
new_bit ^= (noise_shifter_ >> 1);
break;
case Noise16:
new_bit ^= (noise_shifter_ >> 3);
break;
}
noise_shifter_ >>= 1;
noise_shifter_ |= (new_bit & 1) << (shifter_is_16bit_ ? 15 : 14);
}
evaluate_output_volume();
for(int ic = 0; ic < 8 && c < number_of_samples; ic++) {
target[c] = output_volume_;
c++;
master_divider_++;
}
}
master_divider_ &= 7;
}

View File

@ -20,10 +20,38 @@ class SN76489: public Outputs::Speaker::SampleSource {
SN76489(Concurrency::DeferringAsyncTaskQueue &task_queue);
/// Writes a new value to the SN76489.
void write(uint8_t value);
void set_register(uint8_t value);
// As per SampleSource.
void get_samples(std::size_t number_of_samples, std::int16_t *target);
private:
std::size_t master_divider_ = 0;
int16_t output_volume_ = 0;;
void evaluate_output_volume();
int volumes_[16];
Concurrency::DeferringAsyncTaskQueue &task_queue_;
struct ToneChannel {
// Programmatically-set state; updated by the processor.
uint16_t divider = 0;
uint8_t volume = 0xf;
// Active state; self-evolving as a function of time.
uint16_t counter = 0;
int level = 0;
} channels_[4];
enum {
Periodic15,
Periodic16,
Noise15,
Noise16
} noise_mode_ = Periodic15;
uint16_t noise_shifter_ = 0;
int active_register_ = 0;
bool shifter_is_16bit_ = false;
};
}

View File

@ -66,7 +66,7 @@ class Joystick: public Inputs::Joystick {
case '6': mask = 0xe; break;
default: break;
}
keypad_ = (keypad_ & 0xf0) | (mask ^ 0xf);
keypad_ = (keypad_ & 0xf0) | mask;
}
break;
@ -109,6 +109,7 @@ class ConcreteMachine:
z80_(*this),
sn76489_(audio_queue_),
speaker_(sn76489_) {
speaker_.set_input_rate(3579545.0f); // TODO: try to find out whether this is correct.
set_clock_rate(3579545);
joysticks_.emplace_back(new Joystick);
joysticks_.emplace_back(new Joystick);
@ -132,7 +133,7 @@ class ConcreteMachine:
}
Outputs::Speaker::Speaker *get_speaker() override {
return nullptr;
return &speaker_;
}
void run_for(const Cycles cycles) override {
@ -171,10 +172,15 @@ class ConcreteMachine:
// MARK: Z80::BusHandler
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
time_since_vdp_update_ += cycle.length;
time_since_sn76489_update_ += cycle.length;
if(time_until_interrupt_ > 0) {
time_until_interrupt_ -= cycle.length;
if(time_until_interrupt_ <= HalfCycles(0)) {
z80_.set_non_maskable_interrupt_line(true, time_until_interrupt_);
update_video();
time_until_interrupt_ = vdp_->get_time_until_interrupt();
}
}
@ -186,8 +192,8 @@ class ConcreteMachine:
*cycle.value = bios_[address];
} else if(address >= 0x6000 && address < 0x8000) {
*cycle.value = ram_[address & 1023];
} else if(address >= 0x8000 && address < 0x8000 + cartridge_.size()) {
*cycle.value = cartridge_[address - 0x8000];
} else if(address >= 0x8000) {
*cycle.value = cartridge_[(address - 0x8000) % cartridge_.size()]; // This probably isn't how 24kb ROMs work?
} else {
*cycle.value = 0xff;
}
@ -202,7 +208,7 @@ class ConcreteMachine:
case CPU::Z80::PartialMachineCycle::Input:
switch((address >> 5) & 7) {
case 5:
vdp_->run_for(time_since_vdp_update_.flush());
update_video();
*cycle.value = vdp_->get_register(address);
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
time_until_interrupt_ = vdp_->get_time_until_interrupt();
@ -232,7 +238,7 @@ class ConcreteMachine:
break;
case 5:
vdp_->run_for(time_since_vdp_update_.flush());
update_video();
vdp_->set_register(address, *cycle.value);
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
time_until_interrupt_ = vdp_->get_time_until_interrupt();
@ -240,7 +246,7 @@ class ConcreteMachine:
case 7:
update_audio();
sn76489_.write(*cycle.value);
sn76489_.set_register(*cycle.value);
break;
default: break;
@ -250,8 +256,6 @@ class ConcreteMachine:
default: break;
}
time_since_vdp_update_ += cycle.length;
time_since_sn76489_update_ += cycle.length;
return HalfCycles(0);
}
@ -265,6 +269,9 @@ class ConcreteMachine:
void update_audio() {
speaker_.run_for(audio_queue_, time_since_sn76489_update_.divide_cycles(Cycles(2)));
}
void update_video() {
vdp_->run_for(time_since_vdp_update_.flush());
}
CPU::Z80::Processor<ConcreteMachine, false, false> z80_;
std::unique_ptr<TI::TMS9918> vdp_;