Skip to content

Commit a53da3d

Browse files
authored
Merge pull request #199 from quartiq/feature/cossin-tuneup
Feature/cossin tuneup
2 parents f6ca79a + de304c5 commit a53da3d

5 files changed

Lines changed: 148 additions & 128 deletions

File tree

.cargo/config

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ rustflags = [
55
# The target (below) defaults to cortex-m4
66
# There currently are two different options to go beyond that:
77
# 1. cortex-m7 has the right flags and instructions (FPU) but no instruction schedule yet
8-
"-C", "target-cpu=cortex-m7",
8+
# "-C", "target-cpu=cortex-m7",
99
# 2. cortex-m4 with the additional fpv5 instructions and a potentially
1010
# better-than-nothing instruction schedule
1111
"-C", "target-feature=+fp-armv8d16",

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
/target
22
/dsp/target
33
.gdb_history
4-
/dsp/src/cossin_table.txt

dsp/build.rs

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,46 @@
1+
use std::env;
12
use std::f64::consts::PI;
23
use std::fs::File;
34
use std::io::prelude::*;
45
use std::path::Path;
56

6-
const TABLE_DEPTH: usize = 8;
7-
const TABLE_SIZE: usize = 1 << TABLE_DEPTH;
8-
// Treat sin and cos as unsigned values since the sign will always be
9-
// positive in the range [0, pi/4).
10-
const SINCOS_MAX: f64 = u16::MAX as f64;
7+
fn write_cossin_table() {
8+
const DEPTH: usize = 7;
119

12-
fn main() {
13-
let path = Path::new("src").join("cossin_table.txt");
14-
let display = path.display();
10+
let out_dir = env::var_os("OUT_DIR").unwrap();
11+
let dest_path = Path::new(&out_dir).join("cossin_table.rs");
12+
let mut file = File::create(dest_path).unwrap();
1513

16-
let mut file = match File::create(&path) {
17-
Err(why) => panic!("failed to write to {}: {}", display, why),
18-
Ok(file) => file,
19-
};
14+
writeln!(file, "pub(crate) const COSSIN_DEPTH: usize = {};", DEPTH)
15+
.unwrap();
16+
write!(
17+
file,
18+
"pub(crate) const COSSIN: [(u16, u16); 1 << COSSIN_DEPTH] = ["
19+
)
20+
.unwrap();
2021

21-
match file.write_all("[\n".as_bytes()) {
22-
Err(why) => panic!("failed to write to {}: {}", display, why),
23-
Ok(_) => (),
24-
}
22+
// Treat sin and cos as unsigned values since the sign will always be
23+
// positive in the range [0, pi/4).
24+
// No headroom for interpolation rounding error (this is needed for
25+
// DEPTH = 6 for example).
26+
const AMPLITUDE: f64 = u16::MAX as f64;
2527

26-
let phase_delta = PI / 4. / TABLE_SIZE as f64;
27-
let phase_offset = phase_delta / 2.;
28-
for i in 0..TABLE_SIZE {
29-
let phase = phase_offset + phase_delta * (i as f64);
30-
let cos = ((phase.cos() - 0.5) * 2. * SINCOS_MAX).round() as u16;
31-
let sin = (phase.sin() * SINCOS_MAX).round() as u16;
32-
let s = format!(" ({}, {}),\n", cos, sin);
33-
match file.write_all(s.as_bytes()) {
34-
Err(why) => panic!("failed to write to {}: {}", display, why),
35-
Ok(_) => (),
28+
for i in 0..(1 << DEPTH) {
29+
// use midpoint samples to save one entry in the LUT
30+
let phase = (PI / 4. / (1 << DEPTH) as f64) * (i as f64 + 0.5);
31+
// add one bit accuracy to cos due to 0.5 < cos(z) <= 1 for |z| < pi/4
32+
let cos = ((phase.cos() - 0.5) * 2. * AMPLITUDE).round() as u16;
33+
let sin = (phase.sin() * AMPLITUDE).round() as u16;
34+
if i % 4 == 0 {
35+
write!(file, "\n ").unwrap();
3636
}
37+
write!(file, " ({}, {}),", cos, sin).unwrap();
3738
}
39+
writeln!(file, "\n];").unwrap();
3840

39-
match file.write_all("]\n".as_bytes()) {
40-
Err(why) => panic!("failed to write to {}: {}", display, why),
41-
Ok(_) => (),
42-
}
41+
println!("cargo:rerun-if-changed=build.rs");
42+
}
43+
44+
fn main() {
45+
write_cossin_table();
4346
}

dsp/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ pub type Complex<T> = (T, T);
1313
/// # Returns
1414
///
1515
/// Shifted and rounded value.
16-
pub fn shift_round(x: i32, shift: i32) -> i32 {
16+
#[inline(always)]
17+
pub fn shift_round(x: i32, shift: usize) -> i32 {
1718
(x + (1 << (shift - 1))) >> shift
1819
}
1920

dsp/src/trig.rs

Lines changed: 112 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,71 @@
1-
use super::{shift_round, Complex};
2-
use core::mem::swap;
1+
use super::Complex;
2+
use core::f64::consts::PI;
33

4-
const PHASE_BITS: i32 = 20;
5-
const LUT_DEPTH: i32 = 8;
6-
const LUT_SIZE: usize = 1 << LUT_DEPTH as usize;
7-
const OCTANT_BITS: i32 = 3;
8-
const INTERPOLATION_BITS: i32 = PHASE_BITS - LUT_DEPTH - OCTANT_BITS;
9-
static COSSIN_TABLE: [(u16, u16); LUT_SIZE] = include!("cossin_table.txt");
10-
11-
// Approximate pi/4 with an integer multiplier and right bit
12-
// shift. The numerator is designed to saturate the i32 range.
13-
const PI_4_NUMERATOR: i32 = 50;
14-
const PI_4_RIGHT_SHIFT: i32 = 6;
4+
include!(concat!(env!("OUT_DIR"), "/cossin_table.rs"));
155

166
/// Compute the cosine and sine of an angle.
7+
/// This is ported from the MiSoC cossin core.
8+
/// (https://github.com/m-labs/misoc/blob/master/misoc/cores/cossin.py)
179
///
1810
/// # Arguments
19-
///
20-
/// `phase` - 20-bit fixed-point phase value.
11+
/// * `phase` - 32-bit phase.
2112
///
2213
/// # Returns
23-
///
2414
/// The cos and sin values of the provided phase as a `Complex<i32>`
25-
/// value.
15+
/// value. With a 7-bit deep LUT there is 1e-5 max and 6e-8 RMS error
16+
/// in each quadrature over 20 bit phase.
2617
pub fn cossin(phase: i32) -> Complex<i32> {
27-
let mut phase = phase;
28-
let octant = (
29-
(phase & (1 << (PHASE_BITS - 1))) >> (PHASE_BITS - 1),
30-
(phase & (1 << (PHASE_BITS - 2))) >> (PHASE_BITS - 2),
31-
(phase & (1 << (PHASE_BITS - 3))) >> (PHASE_BITS - 3),
32-
);
33-
34-
// Mask off octant bits. This leaves the angle in the range [0,
35-
// pi/4).
36-
phase &= (1 << (PHASE_BITS - OCTANT_BITS)) - 1;
37-
38-
if octant.2 == 1 {
18+
// Phase bits excluding the three highes MSB
19+
const OCTANT_BITS: usize = 32 - 3;
20+
21+
// This is a slightly more compact way to compute the four flags for
22+
// octant mapping/unmapping used below.
23+
let mut octant = (phase as u32) >> OCTANT_BITS;
24+
octant ^= octant << 1;
25+
26+
// Mask off octant bits. This leaves the angle in the range [0, pi/4).
27+
let mut phase = phase & ((1 << OCTANT_BITS) - 1);
28+
29+
if octant & 1 != 0 {
3930
// phase = pi/4 - phase
40-
phase = (1 << (INTERPOLATION_BITS + LUT_DEPTH)) - 1 - phase;
31+
phase = (1 << OCTANT_BITS) - 1 - phase;
4132
}
4233

43-
let interpolation: i32 = phase & ((1 << INTERPOLATION_BITS) - 1);
44-
45-
phase >>= INTERPOLATION_BITS;
46-
47-
let (mut cos, mut sin) = {
48-
let lookup = COSSIN_TABLE[phase as usize];
49-
(
50-
// 1/2 < cos(0<=x<=pi/4) <= 1. So, to spread out the cos
51-
// values and use the space more efficiently, we can
52-
// subtract 1/2 and multiply by 2. Therefore, we add 1
53-
// back in here. The sin values must be multiplied by 2 to
54-
// have the same scale as the cos values.
55-
lookup.0 as i32 + u16::MAX as i32,
56-
(lookup.1 as i32) << 1,
57-
)
58-
};
59-
60-
// The phase values used for the LUT are adjusted up by half the
61-
// phase step. The interpolation must accurately reflect this. So,
62-
// an interpolation phase offset less than half the maximum
63-
// involves a negative phase offset. The rest us a non-negative
64-
// phase offset.
65-
let interpolation_factor =
66-
(interpolation - (1 << (INTERPOLATION_BITS - 1))) * PI_4_NUMERATOR;
67-
let dsin = shift_round(
68-
cos * interpolation_factor,
69-
LUT_DEPTH + INTERPOLATION_BITS + PI_4_RIGHT_SHIFT,
70-
);
71-
let dcos = shift_round(
72-
-sin * interpolation_factor,
73-
LUT_DEPTH + INTERPOLATION_BITS + PI_4_RIGHT_SHIFT,
74-
);
75-
76-
cos += dcos;
77-
sin += dsin;
78-
79-
if octant.1 ^ octant.2 == 1 {
80-
swap(&mut sin, &mut cos);
34+
let lookup = COSSIN[(phase >> (OCTANT_BITS - COSSIN_DEPTH)) as usize];
35+
// 1/2 < cos(0 <= x <= pi/4) <= 1: Shift the cos
36+
// values and scale the sine values as encoded in the LUT.
37+
let mut cos = lookup.0 as i32 + u16::MAX as i32;
38+
let mut sin = (lookup.1 as i32) << 1;
39+
40+
// 16 + 1 bits for cos/sin and 15 for dphi to saturate the i32 range.
41+
const ALIGN_MSB: usize = 32 - 16 - 1;
42+
phase >>= OCTANT_BITS - COSSIN_DEPTH - ALIGN_MSB;
43+
phase &= (1 << ALIGN_MSB) - 1;
44+
// The phase values used for the LUT are at midpoint for the truncated phase.
45+
// Interpolate relative to the LUT entry midpoint.
46+
phase -= (1 << (ALIGN_MSB - 1)) - (octant & 1) as i32;
47+
// Fixed point pi/4.
48+
const PI4: i32 = (PI / 4. * (1 << (32 - ALIGN_MSB)) as f64) as i32;
49+
// No rounding bias necessary here since we keep enough low bits.
50+
let dphi = (phase * PI4) >> (32 - ALIGN_MSB);
51+
52+
// Make room for the sign bit.
53+
let dcos = (sin * dphi) >> (COSSIN_DEPTH + 1);
54+
let dsin = (cos * dphi) >> (COSSIN_DEPTH + 1);
55+
56+
cos = (cos << (ALIGN_MSB - 1)) - dcos;
57+
sin = (sin << (ALIGN_MSB - 1)) + dsin;
58+
59+
// Unmap using octant bits.
60+
if octant & 2 != 0 {
61+
core::mem::swap(&mut sin, &mut cos);
8162
}
8263

83-
if octant.0 ^ octant.1 == 1 {
64+
if octant & 4 != 0 {
8465
cos *= -1;
8566
}
8667

87-
if octant.0 == 1 {
68+
if octant & 8 != 0 {
8869
sin *= -1;
8970
}
9071

@@ -94,35 +75,71 @@ pub fn cossin(phase: i32) -> Complex<i32> {
9475
#[cfg(test)]
9576
mod tests {
9677
use super::*;
97-
use core::f64::consts::PI;
98-
9978
#[test]
10079
fn error_max_rms_all_phase() {
101-
let max_amplitude: f64 = ((1 << 15) - 1) as f64;
80+
// Constant amplitude error due to LUT data range.
81+
const AMPLITUDE: f64 = ((1i64 << 31) - (1i64 << 15)) as f64;
82+
const MAX_PHASE: f64 = (1i64 << 32) as f64;
10283
let mut rms_err: Complex<f64> = (0., 0.);
84+
let mut sum_err: Complex<f64> = (0., 0.);
85+
let mut max_err: Complex<f64> = (0., 0.);
86+
let mut sum: Complex<f64> = (0., 0.);
87+
let mut demod: Complex<f64> = (0., 0.);
88+
89+
// use std::{fs::File, io::prelude::*, path::Path};
90+
// let mut file = File::create(Path::new("data.csv")).unwrap();
91+
92+
const PHASE_DEPTH: usize = 20;
93+
94+
for phase in 0..(1 << PHASE_DEPTH) {
95+
let phase = (phase << (32 - PHASE_DEPTH)) as i32;
96+
let have = cossin(phase);
97+
// writeln!(file, " {},{}", have.0, have.1).unwrap();
98+
99+
let have = (have.0 as f64 / AMPLITUDE, have.1 as f64 / AMPLITUDE);
100+
101+
let radian_phase = 2. * PI * phase as f64 / MAX_PHASE;
102+
let want = (radian_phase.cos(), radian_phase.sin());
103103

104-
for i in 0..(1 << PHASE_BITS) {
105-
let phase = i as i32;
106-
let radian_phase: f64 =
107-
2. * PI * (phase as f64 + 0.5) / ((1 << PHASE_BITS) as f64);
108-
109-
let actual: Complex<f64> = (
110-
max_amplitude * radian_phase.cos(),
111-
max_amplitude * radian_phase.sin(),
112-
);
113-
let computed = cossin(phase);
114-
115-
let err = (
116-
computed.0 as f64 / 4. - actual.0,
117-
computed.1 as f64 / 4. - actual.1,
118-
);
119-
rms_err.0 += err.0 * err.0 / (1 << PHASE_BITS) as f64;
120-
rms_err.1 += err.1 * err.1 / (1 << PHASE_BITS) as f64;
121-
122-
assert!(err.0.abs() < 0.89);
123-
assert!(err.1.abs() < 0.89);
104+
sum.0 += have.0;
105+
sum.1 += have.1;
106+
107+
demod.0 += have.0 * want.0 - have.1 * want.1;
108+
demod.1 += have.1 * want.0 + have.0 * want.1;
109+
110+
let err = (have.0 - want.0, have.1 - want.1);
111+
112+
sum_err.0 += err.0;
113+
sum_err.1 += err.1;
114+
115+
rms_err.0 += err.0 * err.0;
116+
rms_err.1 += err.1 * err.1;
117+
118+
max_err.0 = max_err.0.max(err.0.abs());
119+
max_err.1 = max_err.1.max(err.1.abs());
124120
}
125-
assert!(rms_err.0.sqrt() < 0.41);
126-
assert!(rms_err.1.sqrt() < 0.41);
121+
rms_err.0 /= MAX_PHASE;
122+
rms_err.1 /= MAX_PHASE;
123+
124+
println!("sum: {:.2e} {:.2e}", sum.0, sum.1);
125+
println!("demod: {:.2e} {:.2e}", demod.0, demod.1);
126+
println!("sum_err: {:.2e} {:.2e}", sum_err.0, sum_err.1);
127+
println!("rms: {:.2e} {:.2e}", rms_err.0.sqrt(), rms_err.1.sqrt());
128+
println!("max: {:.2e} {:.2e}", max_err.0, max_err.1);
129+
130+
assert!(sum.0.abs() < 4e-10);
131+
assert!(sum.1.abs() < 4e-10);
132+
133+
assert!(demod.0.abs() < 4e-10);
134+
assert!(demod.1.abs() < 4e-10);
135+
136+
assert!(sum_err.0.abs() < 4e-10);
137+
assert!(sum_err.1.abs() < 4e-10);
138+
139+
assert!(rms_err.0.sqrt() < 6e-8);
140+
assert!(rms_err.1.sqrt() < 6e-8);
141+
142+
assert!(max_err.0 < 1.1e-5);
143+
assert!(max_err.1 < 1.1e-5);
127144
}
128145
}

0 commit comments

Comments
 (0)