Last active
December 3, 2019 16:31
-
-
Save bit-hack/b395b35eabe6de7efc6a886f4fb7dfc0 to your computer and use it in GitHub Desktop.
IceStick SN76489 I2S VGM player
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
`default_nettype none | |
`timescale 1ns / 1ps | |
module i2s_master(input CLK, | |
input [15:0] LIN, | |
input [15:0] RIN, | |
output LRCLK, | |
output DATA, | |
output BCLK); | |
// @48Mhz CLK | |
// 48000000 / (44100 * 32) = 34.01 | |
// 6 - bits | |
reg [6:0] up_cnt; | |
reg [5:0] count; | |
reg [31:0] sr; | |
initial begin | |
up_cnt <= 0; | |
count <= 0; | |
sr <= 0; | |
end | |
assign BCLK = (up_cnt > 8) ? 1 : 0; | |
always @(negedge CLK) begin | |
if (up_cnt == 17) begin | |
up_cnt <= 0; | |
count <= count + 5'd1; | |
if (count[4:0] == 5'd0) begin | |
if (count[5] == 1'b0) begin | |
// prep left data | |
sr <= { 1'b0, LIN[15:1], 16'b0 }; | |
end else begin | |
// prep right data | |
sr <= { 1'b0, RIN[15:1], 16'b0 }; | |
end | |
end else begin | |
// shift all left | |
sr <= { sr[30:0], 1'b0 }; | |
end | |
end else begin | |
up_cnt <= up_cnt + 1; | |
end | |
end | |
assign LRCLK = count[5]; | |
assign DATA = sr[31]; | |
endmodule | |
module sn76489(input CLK, | |
input WR, | |
input [7:0] DATA, | |
output [15:0] OUT, | |
input RST); | |
reg [7:0] ldata; | |
// latched oscillator address | |
reg [2:0] addr; | |
// pitch data | |
// f = N / 32n | |
// where N = ref clock in Hz | |
// n = 10bit register | |
reg [9:0] pitch[4]; | |
// volume data | |
// 0001 2db | |
// 0010 4db | |
// 0100 8db | |
// 1000 16db | |
// 1111 0ff | |
reg [3:0] vol[4]; | |
// tone register | |
reg [9:0] tone[4]; | |
reg [15:0] lfsr; | |
// pitch[3] is the Noise Control | |
// .......fNN | |
// were f = FB | |
// N = NF0,NF1 | |
// pre-attenuation output bits: | |
// { lfsr, tone2, tone1, tone0 } | |
reg [2:0] pre_atten; | |
// volume table ROM | |
reg [15:0] volume_table[32]; | |
initial $readmemh("volume_table.txt", volume_table); | |
// mixer output | |
reg [15:0] out; | |
// channel pre-mixer | |
reg [15:0] chan0; | |
reg [15:0] chan1; | |
reg [15:0] chan2; | |
reg [15:0] chan3; | |
// noise clock divider | |
reg [6:0] noise_clk; | |
/* | |
// at start of simulation | |
initial begin | |
integer i=0; | |
for (i=0; i<4; i=i+1) begin | |
pitch[i] <= 10'h0; | |
vol[i] <= 10'h0; | |
tone[i] <= 10'h0; | |
end | |
lfsr <= 16'h0001; | |
pre_atten <= 3'h0; | |
noise_clk <= 7'b0; | |
end | |
*/ | |
// always assign output | |
assign OUT = out; | |
// update noise | |
always @(posedge noise_clk[3]) begin | |
// TODO: This needs needs to happen at /16 rate | |
// Then optional 2/4/8 divide | |
lfsr <= (lfsr == 0) ? 1 : { lfsr[0] ^ lfsr[3], lfsr[15:1] }; | |
end | |
// on data write | |
always @(negedge WR) begin | |
ldata <= DATA; | |
end | |
// on rising edge of clock | |
always @(posedge CLK) begin | |
// update lfsr | |
noise_clk <= noise_clk + 7'd1; | |
// for all three voices | |
if (tone[0] == 10'b0) begin | |
// reset period | |
tone[0] <= pitch[0]; | |
pre_atten[0] = ~pre_atten[0]; | |
end else begin | |
// decrement | |
tone[0] <= tone[0] - 10'd1; | |
end | |
if (tone[1] == 10'b0) begin | |
// reset period | |
tone[1] <= pitch[1]; | |
pre_atten[1] = ~pre_atten[1]; | |
end else begin | |
// decrement | |
tone[1] <= tone[1] - 10'd1; | |
end | |
if (tone[2] == 10'b0) begin | |
// reset period | |
tone[2] <= pitch[2]; | |
pre_atten[2] = ~pre_atten[2]; | |
end else begin | |
// decrement | |
tone[2] <= tone[2] - 10'd1; | |
end | |
// channel attenuation | |
chan0 = volume_table[{pre_atten[0], vol[0]}]; | |
chan1 = volume_table[{pre_atten[1], vol[1]}]; | |
chan2 = volume_table[{pre_atten[2], vol[2]}]; | |
chan3 = volume_table[{lfsr[0], vol[3]}]; | |
// output mixer | |
out <= chan0 + chan1 + chan2 + chan3 + 16'h8000; | |
end | |
always @(negedge CLK or posedge RST) begin | |
if (RST) begin | |
vol[0] <= 4'hf; | |
vol[1] <= 4'hf; | |
vol[2] <= 4'hf; | |
vol[3] <= 4'hf; | |
end else begin | |
// latch R0, R1, R2 | |
addr = ldata[7] ? ldata[6:4] : addr; | |
// if latch data | |
if (ldata[7]) begin | |
// R2 = 0:tone, 1:vol | |
if (ldata[4] == 1'b1 /* R2 */) begin | |
// ......xxxx | |
vol[ldata[6:5]] <= ldata[3:0]; | |
end else begin | |
// ......xxxx | |
pitch[ldata[6:5]][3:0] <= ldata[3:0]; | |
end | |
// non latch data | |
end else begin | |
// R2 = 0:tone, 1:vol | |
if (addr[0] == 1'b1 /* R2 */) begin | |
// ......xxxx | |
vol[addr[2:1]] <= ldata[3:0]; | |
end else begin | |
// xxxxxx.... | |
pitch[addr[2:1]][9:4] <= ldata[5:0]; | |
end | |
end | |
end | |
end | |
endmodule | |
module uart_tx( | |
input clk, // Master clock | |
input rst, // Synchronous reset | |
input [7:0] tx_byte, // Byte to transmit | |
input start, // Start | |
output tx // transmit pin | |
); | |
parameter baud_rate = 9600; | |
parameter sys_clk_freq = 48000000; | |
// receive counts | |
localparam full_cycle = sys_clk_freq / baud_rate; | |
localparam half_cycle = full_cycle / 2; | |
// receive clock | |
localparam clk_bits = $clog2(full_cycle); | |
reg [clk_bits-1:0] rx_clk; | |
// state machine | |
localparam [1:0] | |
STATE_IDLE = 1'd0, | |
STATE_SEND = 1'd1; | |
reg state; | |
reg bit_tx; | |
reg [3:0] bit_cnt; | |
reg [9:0] bit_shift; | |
reg old_start; | |
reg pending_start; | |
assign tx = bit_tx; | |
/* | |
initial begin | |
state <= STATE_IDLE; | |
bit_tx <= 0; | |
bit_shift <= 0; | |
rx_clk <= 0; | |
bit_cnt <= 0; | |
pending_start <= 0; | |
end | |
*/ | |
always @(posedge clk) begin | |
if (rst) begin | |
state <= STATE_IDLE; | |
bit_tx <= 0; | |
bit_shift <= 0; | |
rx_clk <= 0; | |
bit_cnt <= 0; | |
pending_start <= 0; | |
end else begin | |
if (old_start == 1'b0 && start == 1'b1 && pending_start == 0) begin | |
pending_start <= 1'b1; | |
end | |
old_start <= start; | |
case (state) | |
STATE_IDLE: begin | |
bit_cnt <= 0; | |
bit_tx <= pending_start ? 1'b0 : 1'b1; | |
if (pending_start == 1'b1) begin | |
pending_start <= 1'b0; | |
state <= STATE_SEND; | |
rx_clk <= 0; | |
// prepare shift register | |
// start byte end | |
bit_shift <= { 1'b1, tx_byte, 1'b0 }; | |
end | |
end | |
STATE_SEND: begin | |
if (rx_clk == full_cycle) begin | |
if (bit_cnt == 9) begin | |
state <= STATE_IDLE; | |
end else begin | |
rx_clk <= 0; | |
bit_cnt <= bit_cnt + 1; | |
bit_shift <= { 1'b0, bit_shift[9:1] }; | |
end | |
end else begin | |
if (rx_clk == 0) begin | |
// send MSB | |
bit_tx <= bit_shift[0]; | |
end | |
rx_clk <= rx_clk + 1; | |
end | |
end | |
endcase | |
end | |
end | |
endmodule | |
module uart_rx( | |
input clk, // Master clock | |
input rst, // Synchronous reset | |
input rx, // Incoming serial line | |
output received, // Indicates that a byte has been received | |
output [7:0] rx_byte // Byte received | |
); | |
parameter baud_rate = 9600; | |
parameter sys_clk_freq = 48000000; | |
// receive counts | |
localparam full_cycle = sys_clk_freq / baud_rate; | |
localparam half_cycle = full_cycle / 2; | |
localparam quat_cycle = full_cycle / 4; | |
// state machine | |
localparam [1:0] | |
STATE_IDLE = 2'd0, | |
STATE_RECV = 2'd1, | |
STATE_STOP = 2'd2; | |
reg [1:0] state; | |
// receive clock | |
localparam clk_bits = $clog2(full_cycle); | |
reg [clk_bits-1:0] rx_clk; | |
// bytes shifted in | |
reg [3:0] rx_count; | |
// received byte | |
reg [7:0] rx_out; | |
// temporary shift register | |
reg [7:0] rx_shift; | |
reg [clk_bits-1:0] rx_filter; | |
assign rx_byte = rx_out; | |
assign received = (state == STATE_STOP); | |
/* | |
initial begin | |
state <= STATE_IDLE; | |
rx_clk <= 0; | |
rx_count <= 0; | |
rx_out <= 8'd0; | |
rx_shift <= 8'd0; | |
rx_filter <= 0; | |
end | |
*/ | |
always @(posedge clk) begin | |
if (rst) begin | |
state <= STATE_IDLE; | |
rx_clk <= 0; | |
rx_count <= 0; | |
rx_out <= 8'd0; | |
rx_shift <= 8'd0; | |
rx_filter <= 0; | |
end else begin | |
case (state) | |
// in an idle state | |
// wait for rx to drop low for a half cycle so we can be confident we have | |
// a start bit. wait in idle state for a full cycle before proceeding so | |
// we have a full start bit. | |
STATE_IDLE: begin | |
rx_count <= 0; | |
rx_filter <= 0; | |
if (rx == 0 || rx_clk > half_cycle) begin | |
if (rx_clk == full_cycle) begin | |
state <= STATE_RECV; | |
rx_clk <= 0; | |
end else begin | |
rx_clk <= rx_clk + 1; | |
end | |
end else begin | |
// false start reset | |
// this might be too harsh and could be filtered | |
rx_clk <= 0; | |
end | |
end | |
// receive 8 bits that make up our byte. sample the data on the half cycle | |
// so the signal is stable. we use an average filter to smooth and errors. | |
STATE_RECV: begin | |
if (rx_clk == half_cycle) begin | |
// clock in a new byte | |
// 1 or 0 depending on which it was more often during the half cycle | |
rx_shift <= { ( rx_filter > quat_cycle ? 1'b1 : 1'b0), rx_shift[7:1] }; | |
rx_count <= rx_count + 1; | |
rx_clk <= rx_clk + 1; | |
end else begin | |
if (rx_clk == full_cycle) begin | |
rx_clk <= 0; | |
rx_filter <= 0; | |
if (rx_count == 8) begin | |
rx_out <= rx_shift; | |
state <= STATE_STOP; | |
end | |
end else begin | |
rx_clk <= rx_clk + 1; | |
rx_filter <= rx_filter + rx; | |
end | |
end | |
end | |
// parse the stop bit where rx goes high for one cycle. | |
STATE_STOP: begin | |
if (rx_clk == full_cycle || (rx_clk > quat_cycle && rx == 0)) begin | |
state <= STATE_IDLE; | |
rx_clk <= 0; | |
end else begin | |
rx_clk <= rx_clk + 1; | |
end | |
end | |
// we should never get here | |
default: begin | |
state <= STATE_IDLE; | |
end | |
endcase | |
end | |
end | |
endmodule | |
// SDIN PMOD1 | |
// SCLK PMOD2 | |
// LRCLK PMOD3 | |
// MCLK PMOD4 | |
// GND / | |
// VCC / | |
module vgm(input CLK, input RX, output PMOD1, output PMOD2, output PMOD3); | |
wire I2S_LRCLK; | |
wire I2S_DATA; | |
wire I2S_BCLK; | |
wire pll_clk_out; | |
wire BYPASS; | |
wire RESETB; | |
wire LOCKED; | |
wire global_clk; | |
wire rst = ~LOCKED; | |
assign BYPASS = 0; | |
assign RESETB = 1; | |
assign PMOD1 = I2S_DATA; | |
assign PMOD2 = I2S_BCLK; | |
assign PMOD3 = I2S_LRCLK; | |
// PLLOUT = (CLK_IN * (DIVF + 1)) / (2^DIVQ * (DIVR + 1)) | |
// create a 48Mhz clock | |
SB_PLL40_CORE #( | |
.FEEDBACK_PATH("SIMPLE"), | |
.PLLOUT_SELECT("GENCLK"), | |
.DIVR(4'b0000), | |
.DIVF(7'b0011111), | |
.DIVQ(3'b011), | |
.FILTER_RANGE(3'b001) | |
) uut ( | |
.REFERENCECLK (CLK), | |
.PLLOUTGLOBAL (pll_clk_out), // output frequency | |
.BYPASS (BYPASS), | |
.RESETB (RESETB), // active low | |
.LOCK (LOCKED) // active high | |
); | |
// create a global clock buffer | |
SB_GB gclkbuf(.USER_SIGNAL_TO_GLOBAL_BUFFER(pll_clk_out), | |
.GLOBAL_BUFFER_OUTPUT(global_clk)); | |
wire [7:0] data; | |
wire recv; | |
uart_rx #( | |
.baud_rate(9600), | |
.sys_clk_freq(48000000) | |
) | |
my_rx( | |
global_clk, // The master clock for this module | |
1'b0, // Synchronous reset | |
RX, // Incoming serial line | |
recv, // Indicates that a byte has been received | |
data // Byte received | |
); | |
// generate SN76489 clock | |
reg [7:0] sn_cnt; | |
wire sn_clk = (sn_cnt > 96) ? 1 : 0; | |
always @(posedge global_clk) begin | |
if (sn_cnt == 192) begin | |
sn_cnt <= 0; | |
end else begin | |
sn_cnt <= sn_cnt + 1; | |
end | |
end | |
wire [15:0] tone; | |
sn76489 chip(sn_clk, recv, data, tone, 1'b0); | |
// create I2S master | |
i2s_master i2s(global_clk, tone, tone, I2S_LRCLK, I2S_DATA, I2S_BCLK); | |
endmodule |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment