Compare commits

..

2 commits

Author SHA1 Message Date
slederer
598ee8921f tdraudio: add documentation 2025-10-07 01:16:25 +02:00
slederer
5c00dfcec9 tdraudio: add irq_enable flag, add pcmaudio library
runtime: disable interrupts on PTERM
stdlib: check for error state in FileSize
2025-10-07 00:37:53 +02:00
10 changed files with 496 additions and 22 deletions

View file

@ -9,12 +9,13 @@ The interrupt controller uses a single register at address: $980
|_bit_ |15|14|13|12|11|10|09|08|07|06|05|04|03|02|01|00|
|- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |
|_Value_|t |t |t |t |t |t |t |t |- |- |- |- |- |- |p1 |p0 |
|_Value_|t |t |t |t |t |t |t |t |- |- |- |- |- |p2 |p1 |p0 |
|Bitfields|Description|
|---------|-----------|
| _t_ | unsigned 24 bit counter of timer ticks since reset
| _p2_ | IRQ 2 (audio) interrupt pending if 1
| _p1_ | IRQ 1 (timer tick) interrupt pending if 1
| _p0_ | IRQ 0 (UART) interrupt pending if 1

104
doc/tdraudio.md Normal file
View file

@ -0,0 +1,104 @@
# Audio Controller
The audio controller provides four channels of 16-bit PCM audio playback.
It uses multiple registers starting at address $A00.
Each of the four channels has three registers.
For the first channel the register addresses are:
|Address|Description|
|-------|-----------|
| $A00 | Control Register |
| $A01 | Clock Divider Register |
| $A02 | Amplitude Register |
The register addresses for the second channel start at $A04,
the third channel at $A08
and the fourth channel at $A0C.
## Reading the control register
|_bit_ |31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|
|- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |
|_Value_|- |- |- |- |- |- |- |- |- |-|- |- |- |- |- |- |
|_bit_ |15|14|13|12|11|10|09|08|07|06|05|04|03|02|01|00|
|- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |
|_Value_|- |- |- |- |- |- |- |- |- |- |- |i |f | e | p | c |
|Bitfields|Description|
|---------|-----------|
| _i_ | interrupt is enabled for this channel when 1 |
| _f_ | sample buffer is full when 1 |
| _e_ | sample buffer is empty when 1 |
| _p_ | changes from 0 to 1 and vice versa on each sample clock |
| _c_ | channel is enabled if 1 |
## Writing the control register
|_bit_ |31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|
|- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |
|_Value_|- |- |- |- |- |- |- |- |- |-|- |- |- |- |- |- |
|_bit_ |15|14|13|12|11|10|09|08|07|06|05|04|03|02|01|00|
|- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |
|_Value_|- |- |- |- |- |- |- |- |- |- |- |i |- | - | - | c |
|Bitfields|Description|
|---------|-----------|
| _c_ | enable channel if 1, disable if 0 |
| _i_ | enable channel interrupt if 1, disable if 0 |
## Writing the clock divider register
|_bit_ |31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|
|- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |
|_Value_|d |d |d |d |d |d |d |d |d |d|d |d |d |d |d |d |
|_bit_ |15|14|13|12|11|10|09|08|07|06|05|04|03|02|01|00|
|- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |
|_Value_|d |d |d |d |d |d |d |d |d |d|d |d |d |d |d |d |
|Bitfields|Description|
|---------|-----------|
| _d_ | an unsigned 32-bit value for the clock divider |
## Writing the amplitude register
|_bit_ |31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|
|- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |
|_Value_|- |- |- |- |- |- |- |- |- |-|- |- |- |- |- |- |
|_bit_ |15|14|13|12|11|10|09|08|07|06|05|04|03|02|01|00|
|- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |- |
|_Value_|a |a |a |a |a |a |a |a |a |a |a |a |a | a | a | a |
|Bitfields|Description|
|---------|-----------|
| _a_ | an unsigned 16-bit value for the amplitude (sample) value with a bias of 32768 |
## Notes
The clock divider specifies the number of CPU clock ticks between two samples.
Writing to the amplitude register adds the sample value to the sample buffer. The sample buffer is organized as a FIFO with 16 elements.
Amplitude (sample) values are represented as unsigned, biased 16-bit numbers. The bias is 32768, so given an amplitude range of 1.0 to -1.0, a 1.0 is represented by 65535, 0.0 by 32768 and -1.0 by 0.
Interrupt processing needs to be enabled for each channel if required.
An interrupt on any channel will be signalled to the interrupt controller
as IRQ 2. The interrupt service routine should check all running channels
for an emtpy buffer.
If an audio interrupt has occured on a channel, the interrupt enable flag
is cleared for that channel. It needs to be re-enabled in the interrupt service routine.
Interrupts also need to be enabled on the interrupt controller,
and re-enabled there after each interrupt.

47
examples/pcmtest.pas Normal file
View file

@ -0,0 +1,47 @@
{$H1536}
program pcmtest;
uses pcmaudio;
var filename:string;
buf:SndBufPtr;
f:file;
size:integer;
i:integer;
c:char;
sampleRate:integer;
err:integer;
begin
if ParamCount > 0 then
filename := ParamStr(1)
else
begin
write('Filename> ');
readln(filename);
end;
err := 1;
if ParamCount > 1 then
val(ParamStr(2),sampleRate, err);
if err <> 0 then
sampleRate := 16000;
open(f, filename, ModeReadOnly);
size := FileSize(f);
new(buf, size);
buf^ := '';
write('Reading ', size, ' bytes...');
for i := 1 to size do
begin
read(f,c);
AppendChar(buf^,c);
end;
writeln;
close(f);
PlaySample(buf, sampleRate);
dispose(buf);
end.

60
examples/pcmtest2.pas Normal file
View file

@ -0,0 +1,60 @@
{$H1536}
program pcmtest2;
uses pcmaudio;
var filename:string;
buf:SndBufPtr;
f:file;
size:integer;
i:integer;
c:char;
sampleRate:integer;
err:integer;
done:boolean;
begin
if ParamCount > 0 then
filename := ParamStr(1)
else
begin
write('Filename> ');
readln(filename);
end;
err := 1;
if ParamCount > 1 then
val(ParamStr(2), sampleRate, err);
if err > 0 then
sampleRate := 16000;
open(f, filename, ModeReadOnly);
size := FileSize(f);
new(buf, size);
buf^ := '';
write('Reading ', size, ' bytes...');
for i := 1 to size do
begin
read(f,c);
AppendChar(buf^,c);
end;
writeln;
close(f);
SampleQStart(buf, sampleRate);
write('Press Q to stop> ');
done := false;
while not done do
begin
read(c);
if upcase(c) = 'Q' then
done := true
else
writeln('Queue size: ', SampleQSize);
end;
SampleQStop;
dispose(buf);
end.

7
lib/pcmaudio.inc Normal file
View file

@ -0,0 +1,7 @@
type SndBuf = string[32768];
type SndBufPtr = ^SndBuf;
procedure PlaySample(buf:SndBufPtr;sampleRate:integer); external;
procedure SampleQStart(buf:SndBufPtr;sampleRate:integer); external;
procedure SampleQStop; external;
function SampleQSize:integer; external;

241
lib/pcmaudio.s Normal file
View file

@ -0,0 +1,241 @@
.EQU AUDIO_BASE $A00
.EQU IRQC_REG $980
.EQU IRQC_EN $80
; args: sample rate
START_PCMAUDIO:
; calculate clock divider
LOADCP 77000000
SWAP
LOADCP _DIV
CALL
LOADC AUDIO_BASE + 1
SWAP ; put clock divider on ToS
; LOADCP 4812 ; clock divider for 16KHz sample rate
; LOADCP 2406 ; clock divider for 32KHz sample rate
STOREI 1
LOADCP 32768 ; set amplitude to biased 0
STOREI
DROP
LOADC AUDIO_BASE
LOADC 17 ; enable channel, enable interrupt
STOREI
DROP
RET
STOP_AUDIO:
LOADC AUDIO_BASE
LOADC 0
STOREI
DROP
RET
; args: pointer to pascal string, sample rate
.EQU PS_PTR 0
.EQU PS_COUNT 4
.EQU PS_FS 12
PLAYSAMPLE:
FPADJ -PS_FS
LOADCP START_PCMAUDIO
CALL
DUP
LOADI ; get string size from header
SHR ; divide by 4 to get word count
SHR
STORE PS_COUNT
INC 8 ; skip rest of header
STORE PS_PTR ; store sample data pointer
PS_L0:
LOAD PS_PTR ; load pointer
INC.S1.X2Y 4 ; increment and keep old value
STORE PS_PTR ; store incremented value
LOADI ; load 32 bit word
DUP
BROT ; get upper 16 bit word
BROT
LOADCP $FFFF
AND
LOADCP PLAY_1SAMPLE
CALL
LOADCP $FFFF ; get lower 16 bit word
AND
LOADCP PLAY_1SAMPLE
CALL
LOAD PS_COUNT ; load word count
DEC 1 ; decrement
DUP
STORE PS_COUNT
CBRANCH.NZ PS_L0 ; loop if not zero
LOADCP STOP_AUDIO
CALL
FPADJ PS_FS
RET
; play one sample, waiting
; for the clock divider, which
; is visible via the phase flag
; args: 16-bit unsigned sample
PLAY_1SAMPLE:
PLAY1_L0:
LOADC AUDIO_BASE
LOADI
LOADC 8 ; get fifo_full flag
AND
CBRANCH.NZ PLAY1_L0 ; loop if fifo is full
LOADC AUDIO_BASE+2 ; store amplitude value
SWAP
STOREI
DROP
RET
; start interrupt-driven sample playback
; args: pointer to pascal string, sample rate
SAMPLEQSTART:
LOADCP START_PCMAUDIO
CALL
LOADCP SMPLQ_COUNT
OVER
LOADI ; get string size from header
SHR ; divide by 4 to get word count
SHR
STOREI
DROP
LOADCP SMPLQ_PTR
SWAP
INC 8 ; skip rest of header
STOREI ; store sample data pointer
DROP
LOADCP SMPLQ_ISR ; set interrupt handler
STOREREG IV
LOADC IRQC_REG ; enable irq
LOADC IRQC_EN
STOREI
DROP
RET
SAMPLEQSTOP:
LOADCP SMPLQ_PTR
LOADC 0
STOREI
DROP
LOADCP STOP_AUDIO
CALL
LOADC IRQC_REG ; disable irq
LOADC 0
STOREI
DROP
RET
SAMPLEQSIZE:
LOADCP SMPLQ_COUNT
LOADI
RET
SMPLQ_PTR: .WORD 0
SMPLQ_COUNT: .WORD 0
SMPLQ_ISR:
LOADC IRQC_REG
LOADI
LOADC 4 ; check for audio interrupt
AND
CBRANCH.Z SMPLQ_I_XT ; if flag not set, exit
SMPLQ_I_L:
LOADCP SMPLQ_PTR
LOADI ; load word pointer
DUP
CBRANCH.NZ SMPLQ_I_B ; check for null pointer
DROP
BRANCH SMPLQ_I_XT ; if null, end interrupt routine
SMPLQ_I_B:
LOADI ; load next word
DUP
BROT ; get high half-word
BROT
LOADCP $FFFF
AND
LOADC AUDIO_BASE+2
SWAP
STOREI ; write sample, keep addr
SWAP ; addr to NoS, lower halfword on ToS
LOADCP $FFFF
AND
STOREI ; write sample
DROP
; decrement word count
LOADCP SMPLQ_COUNT
LOADI.S1.X2Y ; load counter, keep addr
DEC 1
DUP
CBRANCH.Z SMPLQ_I_END ; end if zero
STOREI ; store new counter value
DROP
; increment pointer
LOADCP SMPLQ_PTR
LOADI.S1.X2Y
INC 4
STOREI
DROP
; check if fifo is full
LOADC AUDIO_BASE
LOADI
LOADC 8 ; fifo_full
AND
CBRANCH.Z SMPLQ_I_L ; next sample if not full
LOADC AUDIO_BASE
LOADC 17 ; re-enable channel interrupt
STOREI
DROP
BRANCH SMPLQ_I_XT
; end playback, set ptr and counter to zero
SMPLQ_I_END:
DROP
DROP
LOADCP SMPLQ_PTR
LOADC 0
STOREI
DROP
LOADCP SMPLQ_COUNT
LOADC 0
STOREI
DROP
SMPLQ_I_XT:
LOADC IRQC_REG ; re-enable interrupts
LOADC IRQC_EN
STOREI
DROP
LOADREG IR ; jump via interrupt return register
JUMP

View file

@ -1931,6 +1931,12 @@ _CLEARESTACK_XT:
; Terminate program: clear estack and
; jump to coreloader
PTERM:
; just to be safe, disable interrupts
LOADC $980
LOADC 0
STOREI
DROP
LOADCP _CLEARESTACK
CALL
LOADCP LOADER_START

View file

@ -1844,6 +1844,8 @@ end;
function filesize(var fil:file):integer;
begin
checkerror(fil);
if fil.typ = IOChannel then
filesize := -1
else

View file

@ -42,12 +42,12 @@ module wavegen #(DATA_WIDTH=32, CLOCK_DIV_WIDTH=22,
assign fifo_rd_en = (div_count == 0) && channel_enable && ~fifo_empty;
assign fifo_wr_en = wr_en && (reg_sel == TDRAU_REG_AMP);
reg irq_buf, irq_done;
assign irq = irq_buf;
reg irq_buf, irq_enable;
assign irq = channel_enable && irq_buf;
reg [DATA_WIDTH-1:0] rd_data_buf;
assign rd_data = rd_data_buf;
// assign rd_data = {{DATA_WIDTH-8{1'b0}}, {4{1'b0}}, fifo_full, fifo_empty, amp_phase, channel_enable};
assign amp_val = amp_out;
assign running = channel_enable;
@ -56,7 +56,8 @@ module wavegen #(DATA_WIDTH=32, CLOCK_DIV_WIDTH=22,
/* update read data buffer */
always @(posedge clk)
begin
rd_data_buf <= {{DATA_WIDTH-8{1'b0}}, {4{1'b0}}, fifo_full, fifo_empty, amp_phase, channel_enable};
rd_data_buf <= {{DATA_WIDTH-8{1'b0}},
{3{1'b0}}, irq_enable, fifo_full, fifo_empty, amp_phase, channel_enable};
end
/* irq signal to interrupt controller */
@ -65,23 +66,23 @@ module wavegen #(DATA_WIDTH=32, CLOCK_DIV_WIDTH=22,
if(reset)
irq_buf <= 0;
else
if(fifo_empty && ~irq_done)
if(fifo_empty && irq_enable)
irq_buf <= 1;
else
irq_buf <= 0;
end
/* interrupt done flag, used to ensure the irq signal is set for just one clock tick */
/* interrupt enable flag */
always @(posedge clk)
begin
if(reset)
irq_done <= 0;
irq_enable <= 0;
else
if(rd_en) // reset irq done flag on any register read
irq_done <= 0;
if(ctl_reg_write)
irq_enable <= wr_data[4];
else
if(irq_buf)
irq_done <= 1;
irq_enable <= 0; // disable interrupts after an interrupt
end
/* channel enable flag */
@ -139,8 +140,8 @@ module wavegen #(DATA_WIDTH=32, CLOCK_DIV_WIDTH=22,
amp_out <= AMP_BIAS;
// reset phase bit when enabling the channel
if (ctl_reg_write && wr_data[0])
// when channel is enabled, phase will be flipped on next tick
if (ctl_reg_write && wr_data[0] && ~channel_enable)
// when channel is being enabled, phase will be flipped on next tick
// because div_count will become zero
amp_phase <= 1;
end
@ -232,9 +233,6 @@ module tdraudio #(DATA_WIDTH=32) (
chan3_amp,
chan3_running, chan3_irq);
reg irq_out_buf;
assign irq_out = irq_out_buf;
reg [DAC_WIDTH:0] deltasigma_acc; // one extra bit
wire [DAC_WIDTH:0] amp_sum = chan0_amp + chan1_amp + chan2_amp + chan3_amp; // also one overflow bit here
assign gain_sel = 1; // gain select: 0 -> 12dB, 1 -> 6dB
@ -242,6 +240,9 @@ module tdraudio #(DATA_WIDTH=32) (
// assign shutdown_n = running;
assign shutdown_n = 1; /* don't enable shutdown mode, it creates a mains hum */
reg irq_out_buf;
assign irq_out = irq_out_buf;
always @(posedge clk) irq_out_buf <= chan0_irq || chan1_irq || chan2_irq || chan3_irq;
/* delta-sigma DAC */

View file

@ -378,16 +378,18 @@
<Report Name="ROUTE_DESIGN.REPORT_METHODOLOGY" Enabled="1"/>
<RQSFiles/>
</Run>
<Run Id="impl_1" Type="Ft2:EntireDesign" Part="xc7a35ticsg324-1L" ConstrsSet="constrs_1" Description="Includes alternate algorithms for timing-driven optimization" AutoIncrementalCheckpoint="false" WriteIncrSynthDcp="false" State="current" Dir="$PRUNDIR/impl_1" SynthRun="synth_1" IncludeInArchive="true" IsChild="false" GenFullBitstream="true" AutoIncrementalDir="$PSRCDIR/utils_1/imports/impl_1" LaunchOptions="-jobs 6 " AutoRQSDir="$PSRCDIR/utils_1/imports/impl_1" ParallelReportGen="true">
<Run Id="impl_1" Type="Ft2:EntireDesign" Part="xc7a35ticsg324-1L" ConstrsSet="constrs_1" Description="Similar to Performance_ExplorePostRoutePhysOpt, but enables logic optimization step (opt_design) with the ExploreWithRemap directive." AutoIncrementalCheckpoint="false" WriteIncrSynthDcp="false" State="current" Dir="$PRUNDIR/impl_1" SynthRun="synth_1" IncludeInArchive="true" IsChild="false" GenFullBitstream="true" AutoIncrementalDir="$PSRCDIR/utils_1/imports/impl_1" LaunchOptions="-jobs 6 " AutoRQSDir="$PSRCDIR/utils_1/imports/impl_1" ParallelReportGen="true">
<Strategy Version="1" Minor="2">
<StratHandle Name="Performance_ExtraTimingOpt" Flow="Vivado Implementation 2024">
<Desc>Includes alternate algorithms for timing-driven optimization</Desc>
<StratHandle Name="Performance_ExploreWithRemap" Flow="Vivado Implementation 2024">
<Desc>Similar to Performance_ExplorePostRoutePhysOpt, but enables logic optimization step (opt_design) with the ExploreWithRemap directive.</Desc>
</StratHandle>
<Step Id="init_design"/>
<Step Id="opt_design"/>
<Step Id="opt_design">
<Option Id="Directive">6</Option>
</Step>
<Step Id="power_opt_design"/>
<Step Id="place_design">
<Option Id="Directive">8</Option>
<Option Id="Directive">0</Option>
</Step>
<Step Id="post_place_power_opt_design"/>
<Step Id="phys_opt_design">
@ -395,8 +397,11 @@
</Step>
<Step Id="route_design">
<Option Id="Directive">1</Option>
<Option Id="MoreOptsStr"><![CDATA[-tns_cleanup]]></Option>
</Step>
<Step Id="post_route_phys_opt_design" EnableStepBool="1">
<Option Id="Directive">0</Option>
</Step>
<Step Id="post_route_phys_opt_design"/>
<Step Id="write_bitstream">
<Option Id="BinFile">1</Option>
</Step>