mirror of
https://github.com/DrasLorus/CORDIC_Rotate_APFX.git
synced 2024-11-08 22:33:17 +01:00
Working AP template cordic
This commit is contained in:
parent
d46fbc40b6
commit
f2a7c0c886
13 changed files with 950 additions and 200011 deletions
|
@ -59,9 +59,12 @@ if ((NOT EXISTS ${AP_INCLUDE_DIR}/ap_int.h) OR (NOT EXISTS
|
|||
)
|
||||
endif ()
|
||||
|
||||
add_library (cordic STATIC sources/CCordicRotate/CCordicRotate.cpp)
|
||||
add_subdirectory(RomGenerators)
|
||||
|
||||
add_library (cordic STATIC sources/CCordicRotate/CCordicRotate.cpp sources/CCordicRotateHalfPiRom/CCordicRotateHalfPiRom.cpp)
|
||||
target_include_directories (cordic PUBLIC sources)
|
||||
target_include_directories (cordic SYSTEM PUBLIC ${AP_INCLUDE_DIR})
|
||||
target_link_libraries(cordic PUBLIC romgen)
|
||||
|
||||
find_package(Catch2 REQUIRED)
|
||||
|
||||
|
@ -70,3 +73,7 @@ target_link_libraries(catch_common PUBLIC Catch2::Catch2)
|
|||
|
||||
add_executable(cordic_tb sources/tb/cordic_tb.cpp)
|
||||
target_link_libraries(cordic_tb PUBLIC cordic catch_common)
|
||||
|
||||
include(CTest)
|
||||
include(Catch)
|
||||
catch_discover_tests(cordic_tb)
|
||||
|
|
28
RomGenerators/CMakeLists.txt
Normal file
28
RomGenerators/CMakeLists.txt
Normal file
|
@ -0,0 +1,28 @@
|
|||
cmake_minimum_required(VERSION 3.16.0 FATAL_ERROR)
|
||||
# setting this is required
|
||||
|
||||
set(CMAKE_CXX_STANDARD 14)
|
||||
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/../lib)
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/../lib)
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/../bin)
|
||||
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS true)
|
||||
|
||||
project(
|
||||
CordicRomGenerator
|
||||
LANGUAGES CXX
|
||||
VERSION 0.1)
|
||||
|
||||
add_library(romgen sources/RomGeneratorMCHalfPi/RomGeneratorMCHalfPi.cpp
|
||||
sources/RomGeneratorConst/RomGeneratorConst.cpp)
|
||||
target_include_directories(romgen PUBLIC sources)
|
||||
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL
|
||||
"RelWithDebInfo")
|
||||
target_compile_definitions(romgen PRIVATE DEBUG=1)
|
||||
else()
|
||||
target_compile_definitions(romgen PRIVATE NDEBUG=1)
|
||||
endif()
|
||||
|
||||
add_executable(rom_generator sources/main.cpp)
|
||||
target_link_libraries(rom_generator PUBLIC romgen)
|
|
@ -0,0 +1 @@
|
|||
#include "RomGeneratorConst.hpp"
|
148
RomGenerators/sources/RomGeneratorConst/RomGeneratorConst.hpp
Normal file
148
RomGenerators/sources/RomGeneratorConst/RomGeneratorConst.hpp
Normal file
|
@ -0,0 +1,148 @@
|
|||
#ifndef _ROM_GENERATOR_CONST_
|
||||
#define _ROM_GENERATOR_CONST_
|
||||
|
||||
#include <array>
|
||||
#include <climits>
|
||||
#include <cmath>
|
||||
#include <complex>
|
||||
#include <cstdint>
|
||||
|
||||
#include <cassert>
|
||||
|
||||
template <unsigned In_W, unsigned NStages, unsigned Tq>
|
||||
class CRomGeneratorConst {
|
||||
static_assert(In_W > 0, "Inputs can't be on zero bits.");
|
||||
static_assert(NStages < 8, "7 stages of CORDIC is the maximum supported.");
|
||||
static_assert(NStages > 1, "2 stages of CORDIC is the minimum.");
|
||||
|
||||
public:
|
||||
static constexpr double pi = 3.14159265358979323846;
|
||||
static constexpr double two_pi = 2 * pi;
|
||||
static constexpr double half_pi = pi * 0.5;
|
||||
static constexpr double rotation = half_pi;
|
||||
static constexpr double q = Tq;
|
||||
static constexpr uint32_t max_length = 4 * Tq; // 2pi / (pi / 2) * q
|
||||
static constexpr int64_t scale_factor = int64_t(1U << (In_W - 1)); // 2pi / (pi / 2) * q
|
||||
|
||||
static constexpr double atanDbl[28] {
|
||||
0.78539816339745, 0.46364760900081, 0.24497866312686, 0.12435499454676,
|
||||
0.06241880999596, 0.03123983343027, 0.01562372862048, 0.00781234106010,
|
||||
0.00390623013197, 0.00195312251648, 0.00097656218956, 0.00048828121119,
|
||||
0.00024414062015, 0.00012207031189, 0.00006103515617, 0.00003051757812,
|
||||
0.00001525878906, 0.00000762939453, 0.00000381469727, 0.00000190734863,
|
||||
0.00000095367432, 0.00000047683716, 0.00000023841858, 0.00000011920929,
|
||||
0.00000005960464, 0.00000002980232, 0.00000001490116, 0.00000000745058};
|
||||
|
||||
private:
|
||||
constexpr uint8_t cordic_rom_gen(double rot_in) const {
|
||||
|
||||
double A = scale_factor - 1;
|
||||
double B = 0;
|
||||
|
||||
uint8_t R = 0;
|
||||
uint8_t mask = 0x80;
|
||||
|
||||
double beta = rot_in;
|
||||
|
||||
#if 0
|
||||
printf("Step 0 - %03u : %02x : %8lf\n", R, R, beta);
|
||||
#endif
|
||||
if ((beta < -two_pi) || (two_pi <= beta)) {
|
||||
fprintf(stderr, "rotation must be inside ] -2*pi; 2*pi ]");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
if ((beta <= -pi) || (beta > pi)) {
|
||||
beta = beta < 0. ? beta + two_pi : beta - two_pi;
|
||||
}
|
||||
|
||||
if ((beta < -half_pi) || (beta > half_pi)) {
|
||||
R = R | mask;
|
||||
beta = beta < 0 ? beta + pi : beta - pi;
|
||||
// A = -A;
|
||||
// B = -B;
|
||||
} else {
|
||||
R = R & (~mask);
|
||||
}
|
||||
|
||||
for (uint8_t u = 1; u < NStages + 1; u++) {
|
||||
#if 0
|
||||
printf("Step %d - %03u : %02x : %8lf\n", u, R, R, beta);
|
||||
#endif
|
||||
const uint8_t mask = (1U << (7 - u));
|
||||
const uint8_t nmask = ~mask;
|
||||
|
||||
assert((mask & nmask) == 0x00);
|
||||
assert((mask | nmask) == 0xFF);
|
||||
|
||||
const double sigma = beta < 0 ? -1. : 1;
|
||||
|
||||
R = beta < 0 ? R | mask : R & ~mask;
|
||||
|
||||
const double factor = sigma / double(1U << (u - 1));
|
||||
|
||||
const double I = A + B * factor;
|
||||
B = B - A * factor;
|
||||
A = I;
|
||||
|
||||
beta = beta - sigma * atanDbl[u - 1];
|
||||
}
|
||||
#if 0
|
||||
printf("\nFINAL - %03u : %02x : %8lf\n\n", R, R, beta);
|
||||
#endif
|
||||
return R;
|
||||
}
|
||||
|
||||
public:
|
||||
uint8_t rom[max_length];
|
||||
|
||||
constexpr CRomGeneratorConst() {
|
||||
for (unsigned n = 0; n < max_length; n++) {
|
||||
const double chip_rotation = rotation / double(q) * double(n);
|
||||
rom[n] = cordic_rom_gen(chip_rotation);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <unsigned In_W, unsigned NStages, unsigned Tq>
|
||||
void generate_rom_header_const(const char * filename = "rom_cordic.h") {
|
||||
constexpr CRomGeneratorConst<In_W, NStages, Tq> rom;
|
||||
|
||||
FILE * rom_file = fopen(filename, "w");
|
||||
if (!bool(rom_file)) {
|
||||
perror("Can't open the rom file for writing.");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
// fprintf(rom_file, "#ifndef ROM_CORDIC\n#define ROM_CORDIC\n\n");
|
||||
|
||||
fprintf(rom_file, "constexpr uint8_t rom_cordic[%d] = {\n ", rom.max_length);
|
||||
for (uint16_t u = 0; u < rom.max_length - 1; u++) {
|
||||
if (((u & 7) == 0) && u != 0) {
|
||||
fprintf(rom_file, "\n ");
|
||||
}
|
||||
fprintf(rom_file, "%3d, ", uint16_t(rom.rom[u]));
|
||||
}
|
||||
fprintf(rom_file, "%3d};\n", uint16_t(rom.rom[rom.max_length - 1]));
|
||||
|
||||
// fprintf(rom_file, "#endif // ROM_CORDIC");
|
||||
}
|
||||
|
||||
template <unsigned In_W, unsigned NStages, unsigned Tq>
|
||||
void generate_rom_header_const_raw(const char * filename = "rom_cordic.txt") {
|
||||
constexpr CRomGeneratorConst<In_W, NStages, Tq> rom;
|
||||
|
||||
FILE * rom_file = fopen(filename, "w");
|
||||
if (!bool(rom_file)) {
|
||||
perror("Can't open the rom file for writing.");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
for (uint16_t u = 0; u < rom.max_length - 1; u++) {
|
||||
fprintf(rom_file, "%03d\n", uint16_t(rom.rom[u]));
|
||||
}
|
||||
fprintf(rom_file, "%03d\n\n", uint16_t(rom.rom[rom.max_length - 1]));
|
||||
|
||||
}
|
||||
|
||||
#endif // _ROM_GENERATOR_CONST_
|
|
@ -0,0 +1 @@
|
|||
#include "RomGeneratorMCHalfPi.hpp"
|
|
@ -0,0 +1,123 @@
|
|||
#ifndef _ROM_GENERATOR_MC_HALF_PI
|
||||
#define _ROM_GENERATOR_MC_HALF_PI
|
||||
|
||||
#include <climits>
|
||||
#include <cmath>
|
||||
#include <complex>
|
||||
#include <cstdint>
|
||||
|
||||
template <unsigned In_W, unsigned NStages, unsigned Tq>
|
||||
class CRomGeneratorMCHalfPi {
|
||||
static_assert(In_W > 0, "Inputs can't be on zero bits.");
|
||||
static_assert(NStages < 8, "7 stages of CORDIC is the maximum supported.");
|
||||
static_assert(NStages > 1, "2 stages of CORDIC is the minimum.");
|
||||
static_assert(NStages > 1, "2 stages of CORDIC is the minimum.");
|
||||
|
||||
public:
|
||||
static constexpr double rotation = M_PI_2;
|
||||
static constexpr double q = Tq;
|
||||
static constexpr uint32_t max_length = 4 * Tq; // 2pi / (pi / 2) * q
|
||||
static constexpr int64_t scale_factor = int64_t(1U << (In_W - 1)); // 2pi / (pi / 2) * q
|
||||
|
||||
private:
|
||||
constexpr std::complex<int64_t> cordic_MC(const std::complex<int64_t> & x_in,
|
||||
uint8_t counter) {
|
||||
|
||||
int64_t A = x_in.real();
|
||||
int64_t B = x_in.imag();
|
||||
|
||||
const uint8_t R = counter;
|
||||
uint8_t mask = 0x80;
|
||||
if ((R & mask) == mask) {
|
||||
A = -A;
|
||||
B = -B;
|
||||
}
|
||||
|
||||
for (uint16_t u = 1; u < NStages + 1; u++) {
|
||||
mask = mask >> 1;
|
||||
|
||||
const int64_t Ri = (R & mask) == mask ? 1 : -1;
|
||||
|
||||
const int64_t I = A + Ri * (B / int64_t(1U << (u - 1)));
|
||||
B = B - Ri * (A / int64_t(1U << (u - 1)));
|
||||
A = I;
|
||||
}
|
||||
|
||||
return {A, B};
|
||||
}
|
||||
|
||||
public:
|
||||
uint8_t rom[max_length];
|
||||
|
||||
CRomGeneratorMCHalfPi() {
|
||||
for (unsigned n = 0; n < max_length; n++) {
|
||||
const double re_x = floor(double(scale_factor - 1) * cos(-rotation / double(q) * double(n)));
|
||||
const double im_x = floor(double(scale_factor - 1) * sin(-rotation / double(q) * double(n)));
|
||||
|
||||
const std::complex<int64_t> x {re_x, im_x};
|
||||
|
||||
double error = 1000.;
|
||||
uint8_t rom_v = 0x0;
|
||||
|
||||
std::complex<double> res;
|
||||
for (uint32_t v = 0; v < max_length; v++) {
|
||||
const std::complex<int64_t> res_int = cordic_MC(x, v);
|
||||
|
||||
const std::complex<double> res_dbl(double(res_int.real()) / double(scale_factor - 1),
|
||||
double(res_int.imag()) / double(scale_factor - 1));
|
||||
|
||||
const double curr_error = std::abs(std::arg(res_dbl));
|
||||
if (curr_error < error) {
|
||||
error = curr_error;
|
||||
rom_v = uint8_t(v);
|
||||
res = res_dbl;
|
||||
}
|
||||
}
|
||||
|
||||
rom[n] = rom_v;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <unsigned In_W, unsigned NStages, unsigned Tq>
|
||||
void generate_rom_header_mc(const char * filename = "rom_cordic.h") {
|
||||
const CRomGeneratorMCHalfPi<In_W, NStages, Tq> rom;
|
||||
|
||||
FILE * rom_file = fopen(filename, "w");
|
||||
if (!bool(rom_file)) {
|
||||
perror("Can't open the rom file for writing.");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
// fprintf(rom_file, "#ifndef ROM_CORDIC\n#define ROM_CORDIC\n\n");
|
||||
|
||||
fprintf(rom_file, "constexpr uint8_t rom_cordic[%d] = {\n ", rom.max_length);
|
||||
for (uint16_t u = 0; u < rom.max_length - 1; u++) {
|
||||
if (((u & 7) == 0) && u != 0) {
|
||||
fprintf(rom_file, "\n ");
|
||||
}
|
||||
fprintf(rom_file, "%3d, ", uint16_t(rom.rom[u]));
|
||||
}
|
||||
fprintf(rom_file, "%3d};\n", uint16_t(rom.rom[rom.max_length - 1]));
|
||||
|
||||
// fprintf(rom_file, "#endif // ROM_CORDIC");
|
||||
}
|
||||
|
||||
template <unsigned In_W, unsigned NStages, unsigned Tq>
|
||||
void generate_rom_header_mc_raw(const char * filename = "rom_cordic.txt") {
|
||||
const CRomGeneratorMCHalfPi<In_W, NStages, Tq> rom;
|
||||
|
||||
FILE * rom_file = fopen(filename, "w");
|
||||
if (!bool(rom_file)) {
|
||||
perror("Can't open the rom file for writing.");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
for (uint16_t u = 0; u < rom.max_length - 1; u++) {
|
||||
fprintf(rom_file, "%03d\n", uint16_t(rom.rom[u]));
|
||||
}
|
||||
fprintf(rom_file, "%03d\n\n", uint16_t(rom.rom[rom.max_length - 1]));
|
||||
|
||||
}
|
||||
|
||||
#endif // _ROM_GENERATOR_MC_HALF_PI
|
223
RomGenerators/sources/main.cpp
Normal file
223
RomGenerators/sources/main.cpp
Normal file
|
@ -0,0 +1,223 @@
|
|||
#include "RomGeneratorConst/RomGeneratorConst.hpp"
|
||||
#include "RomGeneratorMCHalfPi/RomGeneratorMCHalfPi.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
using namespace std;
|
||||
|
||||
template <unsigned NStages>
|
||||
constexpr complex<int64_t> cordic(complex<int64_t> x_in,
|
||||
uint8_t counter,
|
||||
const uint8_t * rom_cordic) {
|
||||
|
||||
int64_t A = x_in.real();
|
||||
int64_t B = x_in.imag();
|
||||
|
||||
const uint8_t R = rom_cordic[counter];
|
||||
uint8_t mask = 0x80;
|
||||
if ((R & mask) == mask) {
|
||||
A = -A;
|
||||
B = -B;
|
||||
}
|
||||
|
||||
for (uint8_t u = 1; u < NStages + 1; u++) {
|
||||
mask = mask >> 1;
|
||||
|
||||
const int64_t Ri = (R & mask) == mask ? 1 : -1;
|
||||
|
||||
const int64_t I = A + Ri * (B / int64_t(1U << (u - 1)));
|
||||
B = B - Ri * (A / int64_t(1U << (u - 1)));
|
||||
A = I;
|
||||
}
|
||||
|
||||
return {A, B};
|
||||
}
|
||||
|
||||
template <unsigned NStages>
|
||||
void checkMC() {
|
||||
const CRomGeneratorMCHalfPi<16, NStages, 64> rom;
|
||||
|
||||
string fn = "result_MC_W16_S" + to_string(NStages) + "_Q64.dat";
|
||||
ofstream res_file(fn);
|
||||
|
||||
for (unsigned u = 0; u < rom.max_length; u++) {
|
||||
auto res = cordic<NStages>(4096, u, rom.rom);
|
||||
res_file << double(res.real() * 155) / 1048576. << ", " << double(res.imag() * 155) / 1048576. << endl;
|
||||
}
|
||||
}
|
||||
|
||||
template <unsigned NStages>
|
||||
void checkConst() {
|
||||
constexpr CRomGeneratorConst<16, NStages, 64> rom;
|
||||
|
||||
string fn = "result_const_W16_S" + to_string(NStages) + "_Q64.dat";
|
||||
ofstream res_file(fn);
|
||||
|
||||
for (unsigned u = 0; u < rom.max_length; u++) {
|
||||
auto res = cordic<NStages>(4096, u, rom.rom);
|
||||
res_file << double(res.real() * 155) / 1048576. << ", " << double(res.imag() * 155) / 1048576. << endl;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char * argv[]) {
|
||||
|
||||
uint8_t stages;
|
||||
bool use_mc;
|
||||
bool use_txt;
|
||||
|
||||
switch (argc) {
|
||||
case 1:
|
||||
stages = 6;
|
||||
use_mc = false;
|
||||
use_txt = false;
|
||||
break;
|
||||
case 2:
|
||||
stages = stoi(string(argv[1]));
|
||||
use_mc = false;
|
||||
use_txt = false;
|
||||
break;
|
||||
case 3:
|
||||
stages = stoi(string(argv[1]));
|
||||
use_mc = stoi(string(argv[2])) > 0;
|
||||
use_txt = false;
|
||||
break;
|
||||
case 4:
|
||||
stages = stoi(string(argv[1]));
|
||||
use_mc = stoi(string(argv[2])) > 0;
|
||||
use_txt = stoi(string(argv[3])) > 0;
|
||||
break;
|
||||
default:
|
||||
cerr << "Error, too many arguments. Expected at most 3." << endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
if (use_txt) {
|
||||
if (use_mc) {
|
||||
switch (stages) {
|
||||
case 2:
|
||||
generate_rom_header_mc_raw<16, 2, 64>("rom_cordic_mc_W16_S2_Q64.txt");
|
||||
checkMC<2>();
|
||||
break;
|
||||
case 3:
|
||||
generate_rom_header_mc_raw<16, 3, 64>("rom_cordic_mc_W16_S3_Q64.txt");
|
||||
checkMC<3>();
|
||||
break;
|
||||
case 4:
|
||||
generate_rom_header_mc_raw<16, 4, 64>("rom_cordic_mc_W16_S4_Q64.txt");
|
||||
checkMC<4>();
|
||||
break;
|
||||
case 5:
|
||||
generate_rom_header_mc_raw<16, 5, 64>("rom_cordic_mc_W16_S5_Q64.txt");
|
||||
checkMC<5>();
|
||||
break;
|
||||
case 6:
|
||||
generate_rom_header_mc_raw<16, 6, 64>("rom_cordic_mc_W16_S6_Q64.txt");
|
||||
checkMC<6>();
|
||||
break;
|
||||
case 7:
|
||||
generate_rom_header_mc_raw<16, 7, 64>("rom_cordic_mc_W16_S7_Q64.txt");
|
||||
checkMC<7>();
|
||||
break;
|
||||
default:
|
||||
cerr << "Error, no less than 2 stages, no more than 7." << endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
} else {
|
||||
switch (stages) {
|
||||
case 2:
|
||||
generate_rom_header_const_raw<16, 2, 64>("rom_cordic_const_W16_S2_Q64.txt");
|
||||
checkConst<2>();
|
||||
break;
|
||||
case 3:
|
||||
generate_rom_header_const_raw<16, 3, 64>("rom_cordic_const_W16_S3_Q64.txt");
|
||||
checkConst<3>();
|
||||
break;
|
||||
case 4:
|
||||
generate_rom_header_const_raw<16, 4, 64>("rom_cordic_const_W16_S4_Q64.txt");
|
||||
checkConst<4>();
|
||||
break;
|
||||
case 5:
|
||||
generate_rom_header_const_raw<16, 5, 64>("rom_cordic_const_W16_S5_Q64.txt");
|
||||
checkConst<5>();
|
||||
break;
|
||||
case 6:
|
||||
generate_rom_header_const_raw<16, 6, 64>("rom_cordic_const_W16_S6_Q64.txt");
|
||||
checkConst<6>();
|
||||
break;
|
||||
case 7:
|
||||
generate_rom_header_const_raw<16, 7, 64>("rom_cordic_const_W16_S7_Q64.txt");
|
||||
checkConst<7>();
|
||||
break;
|
||||
default:
|
||||
cerr << "Error, no less than 2 stages, no more than 7." << endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
if (use_mc) {
|
||||
switch (stages) {
|
||||
case 2:
|
||||
generate_rom_header_mc<16, 2, 64>("rom_cordic_mc_W16_S2_Q64.hpp");
|
||||
checkMC<2>();
|
||||
break;
|
||||
case 3:
|
||||
generate_rom_header_mc<16, 3, 64>("rom_cordic_mc_W16_S3_Q64.hpp");
|
||||
checkMC<3>();
|
||||
break;
|
||||
case 4:
|
||||
generate_rom_header_mc<16, 4, 64>("rom_cordic_mc_W16_S4_Q64.hpp");
|
||||
checkMC<4>();
|
||||
break;
|
||||
case 5:
|
||||
generate_rom_header_mc<16, 5, 64>("rom_cordic_mc_W16_S5_Q64.hpp");
|
||||
checkMC<5>();
|
||||
break;
|
||||
case 6:
|
||||
generate_rom_header_mc<16, 6, 64>("rom_cordic_mc_W16_S6_Q64.hpp");
|
||||
checkMC<6>();
|
||||
break;
|
||||
case 7:
|
||||
generate_rom_header_mc<16, 7, 64>("rom_cordic_mc_W16_S7_Q64.hpp");
|
||||
checkMC<7>();
|
||||
break;
|
||||
default:
|
||||
cerr << "Error, no less than 2 stages, no more than 7." << endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
} else {
|
||||
switch (stages) {
|
||||
case 2:
|
||||
generate_rom_header_const<16, 2, 64>("rom_cordic_const_W16_S2_Q64.hpp");
|
||||
checkConst<2>();
|
||||
break;
|
||||
case 3:
|
||||
generate_rom_header_const<16, 3, 64>("rom_cordic_const_W16_S3_Q64.hpp");
|
||||
checkConst<3>();
|
||||
break;
|
||||
case 4:
|
||||
generate_rom_header_const<16, 4, 64>("rom_cordic_const_W16_S4_Q64.hpp");
|
||||
checkConst<4>();
|
||||
break;
|
||||
case 5:
|
||||
generate_rom_header_const<16, 5, 64>("rom_cordic_const_W16_S5_Q64.hpp");
|
||||
checkConst<5>();
|
||||
break;
|
||||
case 6:
|
||||
generate_rom_header_const<16, 6, 64>("rom_cordic_const_W16_S6_Q64.hpp");
|
||||
checkConst<6>();
|
||||
break;
|
||||
case 7:
|
||||
generate_rom_header_const<16, 7, 64>("rom_cordic_const_W16_S7_Q64.hpp");
|
||||
checkConst<7>();
|
||||
break;
|
||||
default:
|
||||
cerr << "Error, no less than 2 stages, no more than 7." << endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,5 @@
|
|||
#include "CCordicRotate.hpp"
|
||||
|
||||
|
||||
#define uint2int(sz, in) ((in & (1U << sz)) == (1U << sz) \
|
||||
? static_cast<short>(~in + 1) \
|
||||
: static_cast<short>(in))
|
||||
|
@ -8,14 +7,14 @@
|
|||
template <>
|
||||
void CCordicRotate<8, 14, 4, 17, 5, 19, 7, 12>::process(
|
||||
const ap_fixed<14, 4> & fx_angle,
|
||||
const ap_fixed<17, 5> & fx_re_in,
|
||||
const ap_fixed<17, 5> & fx_im_in,
|
||||
const ap_fixed<17, 5> & fx_re_in,
|
||||
const ap_fixed<17, 5> & fx_im_in,
|
||||
ap_fixed<19, 7> & fx_re_out,
|
||||
ap_fixed<19, 7> & fx_im_out) {
|
||||
|
||||
constexpr uint64_t sign_mask_14 = 0x2000; // 0bxxx xx10 0000 0000 0000
|
||||
constexpr uint64_t sign_mask_17 = 0x10000; // 0bxx1 0000 0000 0000 0000
|
||||
constexpr uint64_t sign_mask_19 = 0x10000; // 0b100 0000 0000 0000 0000
|
||||
// constexpr uint64_t sign_mask_14 = 0x2000; // 0bxxx xx10 0000 0000 0000
|
||||
// constexpr uint64_t sign_mask_17 = 0x10000; // 0bxx1 0000 0000 0000 0000
|
||||
// constexpr uint64_t sign_mask_19 = 0x10000; // 0b100 0000 0000 0000 0000
|
||||
|
||||
const uint16_t angle_bits = fx_angle.bits_to_uint64();
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
#include "CCordicRotateHalfPiRom.hpp"
|
||||
|
176
sources/CCordicRotateHalfPiRom/CCordicRotateHalfPiRom.hpp
Normal file
176
sources/CCordicRotateHalfPiRom/CCordicRotateHalfPiRom.hpp
Normal file
|
@ -0,0 +1,176 @@
|
|||
#ifndef C_CORDIC_ROTATE_ROM_HALF_PI_HPP
|
||||
#define C_CORDIC_ROTATE_ROM_HALF_PI_HPP
|
||||
|
||||
#include <climits>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
|
||||
#include <complex>
|
||||
|
||||
#include <ap_fixed.h>
|
||||
#include <ap_int.h>
|
||||
|
||||
#include "RomGeneratorConst/RomGeneratorConst.hpp"
|
||||
|
||||
// ``` GNU Octave
|
||||
// kn_values(X) = prod(1 ./ abs(1 + 1j * 2.^ (-(0:X))))
|
||||
// ```
|
||||
static constexpr double kn_values[7] = {0.70710678118655, 0.632455532033680, 0.613571991077900, 0.608833912517750, 0.607648256256170, 0.607351770141300, 0.607277644093530};
|
||||
|
||||
template <unsigned TIn_W, unsigned TIn_I, unsigned TNStages, unsigned Tq>
|
||||
class CCordicRotateRomHalfPi {
|
||||
static_assert(TIn_W > 0, "Inputs can't be on zero bits.");
|
||||
static_assert(TNStages < 8, "7 stages of CORDIC is the maximum supported.");
|
||||
static_assert(TNStages > 1, "2 stages of CORDIC is the minimum.");
|
||||
|
||||
public:
|
||||
static constexpr CRomGeneratorConst<TIn_W, TNStages, Tq> rom_cordic {};
|
||||
|
||||
static constexpr unsigned In_W = TIn_W;
|
||||
static constexpr unsigned In_I = TIn_I;
|
||||
static constexpr unsigned Out_W = In_W + 2;
|
||||
static constexpr unsigned Out_I = In_I + 2;
|
||||
static constexpr unsigned NStages = TNStages;
|
||||
|
||||
static constexpr uint64_t kn_i = uint64_t(kn_values[NStages - 1] * double(1U << 3)); // 3 bits are enough
|
||||
static constexpr uint64_t in_scale_factor = uint64_t(1U << (In_W - In_I));
|
||||
static constexpr uint64_t out_scale_factor = uint64_t(1U << (Out_W - Out_I));
|
||||
|
||||
static constexpr int64_t scale_cordic(int64_t in) {
|
||||
return in * kn_i / 8U;
|
||||
}
|
||||
|
||||
constexpr std::complex<int64_t> cordic(std::complex<int64_t> x_in,
|
||||
uint8_t counter) const {
|
||||
|
||||
int64_t A = x_in.real();
|
||||
int64_t B = x_in.imag();
|
||||
|
||||
const uint8_t R = rom_cordic.rom[counter];
|
||||
uint8_t mask = 0x80;
|
||||
if ((R & mask) == mask) {
|
||||
A = -A;
|
||||
B = -B;
|
||||
}
|
||||
|
||||
for (uint8_t u = 1; u < NStages + 1; u++) {
|
||||
mask = mask >> 1;
|
||||
|
||||
const int64_t Ri = (R & mask) == mask ? 1 : -1;
|
||||
|
||||
const int64_t I = A + Ri * (B / int64_t(1U << (u - 1)));
|
||||
B = B - Ri * (A / int64_t(1U << (u - 1)));
|
||||
A = I;
|
||||
}
|
||||
|
||||
return {(A), (B)};
|
||||
}
|
||||
|
||||
#ifndef __SYNTHESIS__
|
||||
static constexpr double scale_cordic(double in) {
|
||||
return in * kn_values[NStages - 1];
|
||||
}
|
||||
|
||||
constexpr std::complex<double> cordic(std::complex<double> x_in,
|
||||
uint8_t counter) const {
|
||||
const std::complex<int64_t> fx_x_in(int64_t(x_in.real() * double(in_scale_factor)),
|
||||
int64_t(x_in.imag() * double(in_scale_factor)));
|
||||
|
||||
const std::complex<int64_t> fx_out = cordic(fx_x_in, counter);
|
||||
return {scale_cordic(double(fx_out.real())) / double(out_scale_factor), scale_cordic(double(fx_out.imag())) / double(out_scale_factor)};
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
template <unsigned ap_W>
|
||||
static ap_int<ap_W> scale_cordic(const ap_int<ap_W> & in) {
|
||||
const ap_int<ap_W + 3> tmp = in * ap_uint<3>(kn_i);
|
||||
return ap_int<ap_W>(tmp >> 3);
|
||||
}
|
||||
|
||||
void cordic(const ap_int<In_W> & re_in, const ap_int<In_W> & im_in,
|
||||
const ap_uint<8> & counter,
|
||||
ap_int<Out_W> & re_out, ap_int<Out_W> & im_out) const {
|
||||
|
||||
const ap_uint<6 + 1> R = (rom_cordic.rom[counter] >> (7 - NStages));
|
||||
|
||||
ap_int<Out_W> A = bool(R[NStages]) ? ap_int<In_W>(-re_in) : re_in;
|
||||
ap_int<Out_W> B = bool(R[NStages]) ? ap_int<In_W>(-im_in) : im_in;
|
||||
|
||||
for (uint8_t u = 1; u < 6 + 1; u++) { // 6 stages
|
||||
|
||||
const bool Ri = bool(R[NStages - u]);
|
||||
|
||||
// Results in (X / 2^(u - 1)), meaning only the
|
||||
// Out_W - u LSBs are meaninfull in shifted_X
|
||||
// Can't use range access since 11111111 (-1) would become 00001111 (15).
|
||||
// Would be possible if the loop is manually unrolled, to predict bitsize,
|
||||
// thus directly put 1111 into 4 bits (so still -1).
|
||||
const ap_int<Out_W> shifted_A = A >> (u - 1); // A(Out_W - 1, u - 1);
|
||||
const ap_int<Out_W> shifted_B = B >> (u - 1); // B(Out_W - 1, u - 1);
|
||||
|
||||
const ap_int<Out_W> arc_step_A
|
||||
= Ri
|
||||
? ap_int<Out_W>(-shifted_A)
|
||||
: shifted_A;
|
||||
const ap_int<Out_W> arc_step_B
|
||||
= Ri
|
||||
? shifted_B
|
||||
: ap_int<Out_W>(-shifted_B);
|
||||
|
||||
const ap_int<Out_W + 1> I = A + arc_step_B;
|
||||
B = B + arc_step_A;
|
||||
A = I;
|
||||
}
|
||||
|
||||
re_out = A;
|
||||
im_out = B;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
#if 0
|
||||
template <>
|
||||
inline void CCordicRotateRomHalfPi<16, 4, 6, 64>::cordic(
|
||||
const ap_int<16> & re_in, const ap_int<16> & im_in,
|
||||
const ap_uint<8> & counter,
|
||||
ap_int<Out_W> & re_out, ap_int<Out_W> & im_out) const {
|
||||
|
||||
const ap_uint<6 + 1> R = (rom_cordic.rom[counter.to_uint()] >> (7 - 6));
|
||||
|
||||
ap_int<Out_W> A = bool(R[6]) ? ap_int<16>(-re_in) : re_in;
|
||||
ap_int<Out_W> B = bool(R[6]) ? ap_int<16>(-im_in) : im_in;
|
||||
|
||||
for (uint8_t u = 1; u < 6 + 1; u++) { // 6 stages
|
||||
|
||||
const bool Ri = bool(R[6 - u]);
|
||||
|
||||
// Results in (X / 2^(u - 1)), meaning only the
|
||||
// Out_W - u LSBs are meaninfull in shifted_X
|
||||
// Can't use range access since 11111111 (-1) would become 00001111 (15).
|
||||
// Would be possible if the loop is manually unrolled, to predict bitsize,
|
||||
// thus directly put 1111 into 4 bits (so still -1).
|
||||
const ap_int<Out_W> shifted_A = A >> (u - 1); // A(Out_W - 1, u - 1);
|
||||
const ap_int<Out_W> shifted_B = B >> (u - 1); // B(Out_W - 1, u - 1);
|
||||
|
||||
const ap_int<Out_W> arc_step_A
|
||||
= Ri
|
||||
? ap_int<Out_W>(-shifted_A)
|
||||
: shifted_A;
|
||||
const ap_int<Out_W> arc_step_B
|
||||
= Ri
|
||||
? shifted_B
|
||||
: ap_int<Out_W>(-shifted_B);
|
||||
|
||||
const auto I = A + arc_step_B;
|
||||
B = B + arc_step_A;
|
||||
A = I;
|
||||
}
|
||||
|
||||
re_out = A;
|
||||
im_out = B;
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // C_CORDIC_ROTATE_ROM_HALF_PI_HPP
|
|
@ -1,4 +1,5 @@
|
|||
#include "CCordicRotate/CCordicRotate.hpp"
|
||||
#include "CCordicRotateHalfPiRom/CCordicRotateHalfPiRom.hpp"
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
|
@ -6,11 +7,10 @@
|
|||
|
||||
using namespace std;
|
||||
|
||||
typedef CCordicRotate<8, 14, 4, 17, 5, 19, 7, 12> cordic_t;
|
||||
|
||||
using Catch::Matchers::Floating::WithinAbsMatcher;
|
||||
|
||||
TEST_CASE("_8_14_10_17_5_19_12_12") {
|
||||
TEST_CASE("Adaptive CORDIC work as intended", "[!hide][WIP]") {
|
||||
typedef CCordicRotate<8, 14, 4, 17, 5, 19, 7, 12> cordic_legacy;
|
||||
|
||||
string input_fn = "../data/input.dat"; // _8_14_4_17_5_19_7_12
|
||||
string output_fn = "../data/output.dat"; // _8_14_4_17_5_19_7_12
|
||||
|
@ -52,7 +52,9 @@ TEST_CASE("_8_14_10_17_5_19_12_12") {
|
|||
for (unsigned iter = 0; iter < n_lines; iter++) {
|
||||
// Execute
|
||||
|
||||
cordic_t::process(angles_in[iter], values_re_in[iter], values_im_in[iter], values_re_out[iter], values_im_out[iter]);
|
||||
cordic_legacy::process(angles_in[iter],
|
||||
values_re_in[iter], values_im_in[iter],
|
||||
values_re_out[iter], values_im_out[iter]);
|
||||
|
||||
// Display the results
|
||||
// cout << "Series " << iter;
|
||||
|
@ -69,3 +71,232 @@ TEST_CASE("_8_14_10_17_5_19_12_12") {
|
|||
// int retval = 0;
|
||||
// Return 0 if the test passed
|
||||
}
|
||||
|
||||
TEST_CASE("ROM-based Cordic works with C-Types", "[CORDIC]") {
|
||||
SECTION("W:16 - I:4 - Stages:6 - q:64") {
|
||||
typedef CCordicRotateRomHalfPi<16, 4, 6, 64> cordic_rom;
|
||||
|
||||
string input_fn = "../data/input.dat"; // _8_14_4_17_5_19_7_12
|
||||
string output_fn = "../data/output.dat"; // _8_14_4_17_5_19_7_12
|
||||
|
||||
constexpr unsigned n_lines = 100000;
|
||||
|
||||
complex<double> values_in[n_lines];
|
||||
complex<double> values_out[n_lines];
|
||||
|
||||
complex<double> results[n_lines];
|
||||
|
||||
ofstream FILE;
|
||||
|
||||
ifstream INPUT(input_fn);
|
||||
|
||||
// Init test vector
|
||||
for (unsigned i = 0; i < n_lines; i++) {
|
||||
double a, b, r;
|
||||
INPUT >> a >> b >> r;
|
||||
|
||||
const complex<double> c {a, b};
|
||||
values_in[i] = c;
|
||||
|
||||
constexpr double rotation = cordic_rom::rom_cordic.rotation;
|
||||
constexpr double q = cordic_rom::rom_cordic.q;
|
||||
|
||||
const complex<double> e = exp(complex<double>(0., rotation / q * (i & 255)));
|
||||
results[i] = c * e;
|
||||
}
|
||||
|
||||
INPUT.close();
|
||||
|
||||
// Save the results to a file
|
||||
FILE.open("results.dat");
|
||||
|
||||
constexpr cordic_rom cordic {};
|
||||
|
||||
constexpr double abs_margin = double(1 << cordic.Out_I) * 2. / 100.;
|
||||
|
||||
// Executing the encoder
|
||||
for (unsigned iter = 0; iter < n_lines; iter++) {
|
||||
// Execute
|
||||
|
||||
values_out[iter] = cordic.cordic(values_in[iter], (iter & 255));
|
||||
|
||||
// Display the results
|
||||
// cout << "Series " << iter;
|
||||
// cout << " Outcome: ";
|
||||
|
||||
FILE << values_out[iter].real() << " " << values_out[iter].imag() << " " << results[iter].real() << " " << results[iter].imag() << endl;
|
||||
|
||||
REQUIRE_THAT(values_out[iter].real(), WithinAbsMatcher(results[iter].real(), abs_margin));
|
||||
REQUIRE_THAT(values_out[iter].imag(), WithinAbsMatcher(results[iter].imag(), abs_margin));
|
||||
}
|
||||
FILE.close();
|
||||
|
||||
// Compare the results file with the golden results
|
||||
// int retval = 0;
|
||||
// Return 0 if the test passed
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("ROM-based Cordic works with AP-Types", "[CORDIC]") {
|
||||
constexpr unsigned n_lines = 100000;
|
||||
|
||||
SECTION("W:16 - I:4 - Stages:6 - q:64") {
|
||||
typedef CCordicRotateRomHalfPi<16, 4, 6, 64> cordic_rom;
|
||||
|
||||
string input_fn = "../data/input.dat";
|
||||
|
||||
constexpr double rotation = cordic_rom::rom_cordic.rotation;
|
||||
constexpr double q = cordic_rom::rom_cordic.q;
|
||||
constexpr uint64_t cnt_mask = 0xFF; // Value dependant of the way the ROM is initialized
|
||||
|
||||
constexpr unsigned Out_W = cordic_rom::Out_W;
|
||||
constexpr unsigned In_W = cordic_rom::In_W;
|
||||
|
||||
ap_int<In_W> values_re_in[n_lines];
|
||||
ap_int<In_W> values_im_in[n_lines];
|
||||
ap_int<Out_W> values_re_out[n_lines];
|
||||
ap_int<Out_W> values_im_out[n_lines];
|
||||
|
||||
double results_re[n_lines];
|
||||
double results_im[n_lines];
|
||||
|
||||
ofstream out_stream;
|
||||
|
||||
ifstream INPUT(input_fn);
|
||||
|
||||
// Init test vector
|
||||
for (unsigned i = 0; i < n_lines; i++) {
|
||||
double a, b, r;
|
||||
INPUT >> a >> b >> r;
|
||||
|
||||
const complex<double> c {a, b};
|
||||
values_re_in[i] = int64_t(a * double(cordic_rom::in_scale_factor));
|
||||
values_im_in[i] = int64_t(b * double(cordic_rom::in_scale_factor));
|
||||
|
||||
const complex<double> e = c * exp(complex<double>(0., rotation / q * (i & cnt_mask)));
|
||||
results_re[i] = e.real();
|
||||
results_im[i] = e.imag();
|
||||
}
|
||||
|
||||
INPUT.close();
|
||||
|
||||
// Save the results to a file
|
||||
out_stream.open("results_ap.dat");
|
||||
FILE * romf = fopen("rom.dat", "w");
|
||||
|
||||
constexpr cordic_rom cordic {};
|
||||
|
||||
constexpr double abs_margin = double(1 << cordic.Out_I) * 2. / 100.;
|
||||
|
||||
// Executing the encoder
|
||||
for (unsigned iter = 0; iter < n_lines; iter++) {
|
||||
// Execute
|
||||
const uint8_t counter = uint8_t(iter & cnt_mask);
|
||||
|
||||
if (iter < cnt_mask + 1)
|
||||
fprintf(romf, "%03d\n", (uint16_t) cordic.rom_cordic.rom[counter]);
|
||||
|
||||
cordic.cordic(
|
||||
values_re_in[iter], values_im_in[iter],
|
||||
counter,
|
||||
values_re_out[iter], values_im_out[iter]);
|
||||
|
||||
// Display the results
|
||||
// cout << "Series " << iter;
|
||||
// cout << " Outcome: ";
|
||||
|
||||
out_stream << values_re_out[iter].to_int64() << " " << values_im_out[iter].to_int64() << " " << results_re[iter] << " " << results_im[iter] << endl;
|
||||
|
||||
REQUIRE_THAT(values_re_out[iter].to_double() * 5. / 8. / cordic_rom::out_scale_factor, WithinAbsMatcher(results_re[iter], abs_margin));
|
||||
REQUIRE_THAT(values_im_out[iter].to_double() * 5. / 8. / cordic_rom::out_scale_factor, WithinAbsMatcher(results_im[iter], abs_margin));
|
||||
}
|
||||
out_stream.close();
|
||||
fclose(romf);
|
||||
|
||||
// Compare the results file with the golden results
|
||||
// int retval = 0;
|
||||
// Return 0 if the test passed
|
||||
}
|
||||
|
||||
SECTION("W:16 - I:4 - Stages:6 - q:64 - internal scaling") {
|
||||
typedef CCordicRotateRomHalfPi<16, 4, 6, 64> cordic_rom;
|
||||
|
||||
string input_fn = "../data/input.dat";
|
||||
|
||||
constexpr double rotation = cordic_rom::rom_cordic.rotation;
|
||||
constexpr double q = cordic_rom::rom_cordic.q;
|
||||
constexpr uint64_t cnt_mask = 0xFF; // Value dependant of the way the ROM is initialized
|
||||
|
||||
constexpr unsigned Out_W = cordic_rom::Out_W;
|
||||
constexpr unsigned In_W = cordic_rom::In_W;
|
||||
|
||||
ap_int<In_W> values_re_in[n_lines];
|
||||
ap_int<In_W> values_im_in[n_lines];
|
||||
ap_int<Out_W> values_re_out[n_lines];
|
||||
ap_int<Out_W> values_im_out[n_lines];
|
||||
|
||||
double results_re[n_lines];
|
||||
double results_im[n_lines];
|
||||
|
||||
ofstream out_stream;
|
||||
|
||||
ifstream INPUT(input_fn);
|
||||
|
||||
// Init test vector
|
||||
for (unsigned i = 0; i < n_lines; i++) {
|
||||
double a, b, r;
|
||||
INPUT >> a >> b >> r;
|
||||
|
||||
const complex<double> c {a, b};
|
||||
values_re_in[i] = int64_t(a * double(cordic_rom::in_scale_factor));
|
||||
values_im_in[i] = int64_t(b * double(cordic_rom::in_scale_factor));
|
||||
|
||||
const complex<double> e = c * exp(complex<double>(0., rotation / q * (i & cnt_mask)));
|
||||
results_re[i] = e.real();
|
||||
results_im[i] = e.imag();
|
||||
}
|
||||
|
||||
INPUT.close();
|
||||
|
||||
// Save the results to a file
|
||||
out_stream.open("results_ap.dat");
|
||||
FILE * romf = fopen("rom.dat", "w");
|
||||
|
||||
constexpr cordic_rom cordic {};
|
||||
|
||||
constexpr double abs_margin = double(1 << cordic.Out_I) * 2. / 100.;
|
||||
|
||||
// Executing the encoder
|
||||
for (unsigned iter = 0; iter < n_lines; iter++) {
|
||||
// Execute
|
||||
const uint8_t counter = uint8_t(iter & cnt_mask);
|
||||
|
||||
if (iter < cnt_mask + 1)
|
||||
fprintf(romf, "%03d\n", (uint16_t) cordic.rom_cordic.rom[counter]);
|
||||
|
||||
cordic.cordic(
|
||||
values_re_in[iter], values_im_in[iter],
|
||||
counter,
|
||||
values_re_out[iter], values_im_out[iter]);
|
||||
|
||||
// Display the results
|
||||
// cout << "Series " << iter;
|
||||
// cout << " Outcome: ";
|
||||
|
||||
out_stream << values_re_out[iter].to_int64() << " " << values_im_out[iter].to_int64() << " " << results_re[iter] << " " << results_im[iter] << endl;
|
||||
|
||||
REQUIRE_THAT(cordic_rom::scale_cordic<Out_W>(values_re_out[iter]).to_double() / cordic_rom::out_scale_factor,
|
||||
WithinAbsMatcher(results_re[iter],
|
||||
abs_margin));
|
||||
REQUIRE_THAT(cordic_rom::scale_cordic<Out_W>(values_im_out[iter]).to_double() / cordic_rom::out_scale_factor,
|
||||
WithinAbsMatcher(results_im[iter],
|
||||
abs_margin));
|
||||
}
|
||||
out_stream.close();
|
||||
fclose(romf);
|
||||
|
||||
// Compare the results file with the golden results
|
||||
// int retval = 0;
|
||||
// Return 0 if the test passed
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue