266 lines
8.4 KiB
C++
266 lines
8.4 KiB
C++
/*
|
|
AudioOutputULP
|
|
Outputs to ESP32 DAC through the ULP, freeing I2S for other uses
|
|
|
|
Copyright (C) 2020 Martin Laclaustra, based on bitluni's code
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <Arduino.h>
|
|
#if ESP_IDF_VERSION_MAJOR < 5 // TODO Arduino 3.0 Port I2S
|
|
|
|
#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2
|
|
|
|
#include "AudioOutputULP.h"
|
|
#include <esp32/ulp.h>
|
|
#include <driver/rtc_io.h>
|
|
#include <driver/dac.h>
|
|
#include <soc/rtc.h>
|
|
#include <math.h>
|
|
|
|
uint32_t create_I_WR_REG(uint32_t reg, uint32_t low_bit, uint32_t high_bit, uint32_t val){
|
|
typedef union {ulp_insn_t ulp_ins; uint32_t ulp_bin;} ulp_union;
|
|
const ulp_insn_t singleinstruction[] = {I_WR_REG(reg, low_bit, high_bit, val)};
|
|
ulp_union recover_ins;
|
|
recover_ins.ulp_ins=singleinstruction[0];
|
|
return (uint32_t)(recover_ins.ulp_bin);
|
|
}
|
|
|
|
uint32_t create_I_BXI(uint32_t imm_pc){
|
|
typedef union {ulp_insn_t ulp_ins; uint32_t ulp_bin;} ulp_union;
|
|
const ulp_insn_t singleinstruction[] = {I_BXI(imm_pc)};
|
|
ulp_union recover_ins;
|
|
recover_ins.ulp_ins=singleinstruction[0];
|
|
return (uint32_t)(recover_ins.ulp_bin);
|
|
}
|
|
|
|
bool AudioOutputULP::begin()
|
|
{
|
|
if(!stereoOutput){
|
|
waitingOddSample = false;
|
|
//totalSampleWords += 512;
|
|
//dacTableStart2 = dacTableStart1;
|
|
}
|
|
|
|
//calculate the actual ULP clock
|
|
unsigned long rtc_8md256_period = rtc_clk_cal(RTC_CAL_8MD256, 1000);
|
|
unsigned long rtc_fast_freq_hz = 1000000ULL * (1 << RTC_CLK_CAL_FRACT) * 256 / rtc_8md256_period;
|
|
|
|
//initialize DACs
|
|
if(activeDACs & 1){
|
|
dac_output_enable(DAC_CHANNEL_1);
|
|
dac_output_voltage(DAC_CHANNEL_1, 128);
|
|
}
|
|
if(activeDACs & 2){
|
|
dac_output_enable(DAC_CHANNEL_2);
|
|
dac_output_voltage(DAC_CHANNEL_2, 128);
|
|
}
|
|
|
|
int retAddress1 = 9;
|
|
int retAddress2 = 14;
|
|
|
|
int loopCycles = 134;
|
|
int loopHalfCycles1 = 90;
|
|
int loopHalfCycles2 = 44;
|
|
|
|
Serial.print("Real RTC clock: ");
|
|
Serial.println(rtc_fast_freq_hz);
|
|
|
|
uint32_t dt = (rtc_fast_freq_hz / hertz) - loopCycles;
|
|
uint32_t dt2 = 0;
|
|
if(!stereoOutput){
|
|
dt = (rtc_fast_freq_hz / hertz) - loopHalfCycles1;
|
|
dt2 = (rtc_fast_freq_hz / hertz) - loopHalfCycles2;
|
|
}
|
|
|
|
Serial.print("dt: ");
|
|
Serial.println(dt);
|
|
|
|
Serial.print("dt2: ");
|
|
Serial.println(dt2);
|
|
|
|
const ulp_insn_t stereo[] = {
|
|
//reset offset register
|
|
I_MOVI(R3, 0),
|
|
//delay to get the right sampling rate
|
|
I_DELAY(dt), // 6 + dt
|
|
//reset sample index
|
|
I_MOVI(R0, 0), // 6
|
|
//write the index back to memory for the main cpu
|
|
I_ST(R0, R3, indexAddress), // 8
|
|
//load the samples
|
|
I_LD(R1, R0, bufferStart), // 8
|
|
//mask the lower 8 bits
|
|
I_ANDI(R2, R1, 0x00ff), // 6
|
|
//multiply by 2
|
|
I_LSHI(R2, R2, 1), // 6
|
|
//add start position
|
|
I_ADDI(R2, R2, dacTableStart1),// 6
|
|
//jump to the dac opcode
|
|
I_BXR(R2), // 4
|
|
//back from first dac
|
|
//delay between the two samples in mono rendering
|
|
I_DELAY(dt2), // 6 + dt2
|
|
//mask the upper 8 bits
|
|
I_ANDI(R2, R1, 0xff00), // 6
|
|
//shift the upper bits to right and multiply by 2
|
|
I_RSHI(R2, R2, 8 - 1), // 6
|
|
//add start position of second dac table
|
|
I_ADDI(R2, R2, dacTableStart2),// 6
|
|
//jump to the dac opcode
|
|
I_BXR(R2), // 4
|
|
//here we get back from writing the second sample
|
|
//load 0x8080 as sample
|
|
I_MOVI(R1, 0x8080), // 6
|
|
//write 0x8080 in the sample buffer
|
|
I_ST(R1, R0, indexAddress), // 8
|
|
//increment the sample index
|
|
I_ADDI(R0, R0, 1), // 6
|
|
//if reached end of the buffer, jump relative to index reset
|
|
I_BGE(-16, totalSampleWords), // 4
|
|
//wait to get the right sample rate (2 cycles more to compensate the index reset)
|
|
I_DELAY((unsigned int)dt + 2), // 8 + dt
|
|
//if not, jump absolute to where index is written to memory
|
|
I_BXI(3) // 4
|
|
};
|
|
// write io and jump back another 12 + 4 + 12 + 4
|
|
|
|
size_t load_addr = 0;
|
|
size_t size = sizeof(stereo)/sizeof(ulp_insn_t);
|
|
ulp_process_macros_and_load(load_addr, stereo, &size);
|
|
// this is how to get the opcodes
|
|
// for(int i = 0; i < size; i++)
|
|
// Serial.println(RTC_SLOW_MEM[i], HEX);
|
|
|
|
//create DAC opcode tables
|
|
switch(activeDACs){
|
|
case 1:
|
|
for(int i = 0; i < 256; i++)
|
|
{
|
|
RTC_SLOW_MEM[dacTableStart1 + i * 2] = create_I_WR_REG(RTC_IO_PAD_DAC1_REG,19,26,i); //dac1: 0x1D4C0121 | (i << 10)
|
|
RTC_SLOW_MEM[dacTableStart1 + 1 + i * 2] = create_I_BXI(retAddress1); // 0x80000000 + retAddress1 * 4
|
|
RTC_SLOW_MEM[dacTableStart2 + i * 2] = create_I_WR_REG(RTC_IO_PAD_DAC1_REG,19,26,i); //dac2: 0x1D4C0122 | (i << 10)
|
|
RTC_SLOW_MEM[dacTableStart2 + 1 + i * 2] = create_I_BXI(retAddress2); // 0x80000000 + retAddress2 * 4
|
|
}
|
|
break;
|
|
case 2:
|
|
for(int i = 0; i < 256; i++)
|
|
{
|
|
RTC_SLOW_MEM[dacTableStart1 + i * 2] = create_I_WR_REG(RTC_IO_PAD_DAC2_REG,19,26,i); //dac1: 0x1D4C0121 | (i << 10)
|
|
RTC_SLOW_MEM[dacTableStart1 + 1 + i * 2] = create_I_BXI(retAddress1); // 0x80000000 + retAddress1 * 4
|
|
RTC_SLOW_MEM[dacTableStart2 + i * 2] = create_I_WR_REG(RTC_IO_PAD_DAC2_REG,19,26,i); //dac2: 0x1D4C0122 | (i << 10)
|
|
RTC_SLOW_MEM[dacTableStart2 + 1 + i * 2] = create_I_BXI(retAddress2); // 0x80000000 + retAddress2 * 4
|
|
}
|
|
break;
|
|
case 3:
|
|
for(int i = 0; i < 256; i++)
|
|
{
|
|
RTC_SLOW_MEM[dacTableStart1 + i * 2] = create_I_WR_REG(RTC_IO_PAD_DAC1_REG,19,26,i); //dac1: 0x1D4C0121 | (i << 10)
|
|
RTC_SLOW_MEM[dacTableStart1 + 1 + i * 2] = create_I_BXI(retAddress1); // 0x80000000 + retAddress1 * 4
|
|
RTC_SLOW_MEM[dacTableStart2 + i * 2] = create_I_WR_REG(RTC_IO_PAD_DAC1_REG,19,26,i); //dac2: 0x1D4C0122 | (i << 10)
|
|
RTC_SLOW_MEM[dacTableStart2 + 1 + i * 2] = create_I_BXI(retAddress2); // 0x80000000 + retAddress2 * 4
|
|
}
|
|
break;
|
|
}
|
|
|
|
//set all samples to 128 (silence)
|
|
for(int i = 0; i < totalSampleWords; i++)
|
|
RTC_SLOW_MEM[bufferStart + i] = 0x8080;
|
|
|
|
//start
|
|
RTC_SLOW_MEM[indexAddress] = 0;
|
|
ulp_run(0);
|
|
|
|
//wait until ULP starts using samples and the index of output sample advances
|
|
while(RTC_SLOW_MEM[indexAddress] == 0)
|
|
delay(1);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool AudioOutputULP::ConsumeSample(int16_t sample[2])
|
|
{
|
|
int16_t ms[2];
|
|
ms[0] = sample[0];
|
|
ms[1] = sample[1];
|
|
MakeSampleStereo16( ms );
|
|
|
|
// TODO: needs improvement (counting is different here with respect to ULP code)
|
|
int currentSample = RTC_SLOW_MEM[indexAddress] & 0xffff;
|
|
int currentWord = currentSample >> 1;
|
|
|
|
for (int i=0; i<2; i++) {
|
|
ms[i] = ((ms[i] >> 8) + 128) & 0xff;
|
|
}
|
|
if(!stereoOutput) // mix both channels
|
|
ms[0] = (uint16_t)(( (uint32_t)((int32_t)(ms[0]) + (int32_t)(ms[1])) >> 1 ) & 0xff);
|
|
|
|
if(waitingOddSample){ // always true for stereo because samples are consumed in pairs
|
|
if(lastFilledWord != currentWord) // accept sample if writing index lastFilledWord has not reached index of output sample
|
|
{
|
|
unsigned int w;
|
|
if(stereoOutput){
|
|
w = ms[0];
|
|
w |= ms[1] << 8;
|
|
} else {
|
|
w = bufferedOddSample;
|
|
w |= ms[0] << 8;
|
|
bufferedOddSample = 128;
|
|
waitingOddSample = false;
|
|
}
|
|
RTC_SLOW_MEM[bufferStart + lastFilledWord] = w;
|
|
lastFilledWord++;
|
|
if(lastFilledWord == totalSampleWords)
|
|
lastFilledWord = 0;
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
bufferedOddSample = ms[0];
|
|
waitingOddSample = true;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
|
|
bool AudioOutputULP::stop()
|
|
{
|
|
audioLogger->printf_P(PSTR("\n\n\nstop\n\n\n"));
|
|
const ulp_insn_t stopulp[] = {
|
|
//stop the timer
|
|
I_END(),
|
|
//end the program
|
|
I_HALT()};
|
|
|
|
size_t load_addr = 0;
|
|
size_t size = sizeof(stopulp)/sizeof(ulp_insn_t);
|
|
ulp_process_macros_and_load(load_addr, stopulp, &size);
|
|
|
|
//start
|
|
ulp_run(0);
|
|
|
|
if(activeDACs & 1){
|
|
dac_output_voltage(DAC_CHANNEL_1, 128);
|
|
}
|
|
if(activeDACs & 2){
|
|
dac_output_voltage(DAC_CHANNEL_2, 128);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#endif
|
|
#endif // ESP_IDF_VERSION_MAJOR < 5 // TODO Arduino 3.0 Port I2S
|