Klaudiusz ugotował
This commit is contained in:
+7
-4
@@ -1,11 +1,14 @@
|
|||||||
[build]
|
[build]
|
||||||
target = "thumbv6m-none-eabi"
|
target = "thumbv8m.main-none-eabihf"
|
||||||
|
# target = "thumbv6m-none-eabi"
|
||||||
|
|
||||||
[env]
|
[env]
|
||||||
DEFMT_LOG = "trace"
|
DEFMT_LOG = "debug"
|
||||||
|
|
||||||
[target.thumbv6m-none-eabi]
|
# [target.thumbv6m-none-eabi]
|
||||||
runner = 'probe-rs run --chip STM32G0B1RE'
|
[target.thumbv8m.main-none-eabihf]
|
||||||
|
# runner = 'probe-rs run --chip STM32G0B1RE'
|
||||||
|
runner = 'probe-rs run --chip STM32U585CI'
|
||||||
rustflags = [
|
rustflags = [
|
||||||
"-C", "link-arg=--nmagic",
|
"-C", "link-arg=--nmagic",
|
||||||
"-C", "link-arg=-Tlink.x",
|
"-C", "link-arg=-Tlink.x",
|
||||||
|
|||||||
+2
-1
@@ -21,7 +21,8 @@ embassy-stm32 = {
|
|||||||
"rt",
|
"rt",
|
||||||
"defmt",
|
"defmt",
|
||||||
"memory-x",
|
"memory-x",
|
||||||
"stm32g0b1re",
|
# "stm32g0b1re",
|
||||||
|
"stm32u585ci",
|
||||||
"time-driver-any",
|
"time-driver-any",
|
||||||
"exti",
|
"exti",
|
||||||
"unstable-pac",
|
"unstable-pac",
|
||||||
|
|||||||
+30
-5
@@ -1,27 +1,52 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Render a sensor dump (one bracketed row of numbers per line) as a heatmap.
|
"""Render a sensor dump as a heatmap.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python heatmap.py [input.txt] [-o out.png] [--cmap inferno]
|
python heatmap.py [input.txt] [-o out.png] [--cmap inferno]
|
||||||
|
|
||||||
Each line is expected to look like:
|
Accepts either:
|
||||||
|
- Raw device log lines as printed by the firmware, e.g.:
|
||||||
|
5.464019 [INFO ] row 0: [71.6, 75.2, ..., 82.1] (tts_test tts-test/src/main.rs:212)
|
||||||
|
Only the "row N: [...]" part of each line is used; everything else
|
||||||
|
(timestamps, log level, source location, other info! lines like the
|
||||||
|
ambient temperature/heatmap range) is ignored. You can paste the whole
|
||||||
|
console log as-is.
|
||||||
|
- Plain rows of numbers, one row per line, brackets/commas optional:
|
||||||
[55, 192, 131, ..., 138]
|
[55, 192, 131, ..., 138]
|
||||||
Brackets/commas are optional, so plain whitespace-separated rows also work.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
|
||||||
|
_ROW_RE = re.compile(r"row\s*(\d+)\s*:\s*\[([^\]]*)\]", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
def parse_grid(text):
|
def parse_grid(text):
|
||||||
"""Parse lines of numbers into a 2D list of floats."""
|
"""Parse a device log or a plain number dump into a 2D list of floats."""
|
||||||
|
text = _ANSI_RE.sub("", text)
|
||||||
|
|
||||||
|
# Prefer explicit "row N: [...]" entries (as printed by the firmware's
|
||||||
|
# info! logs) so that timestamps, log levels and source locations on the
|
||||||
|
# same line don't get mistaken for pixel data.
|
||||||
|
rows = {}
|
||||||
|
for match in _ROW_RE.finditer(text):
|
||||||
|
idx = int(match.group(1))
|
||||||
|
nums = re.findall(r"-?\d+(?:\.\d+)?", match.group(2))
|
||||||
|
if nums:
|
||||||
|
rows[idx] = [float(n) for n in nums]
|
||||||
|
|
||||||
|
if rows:
|
||||||
|
grid = [rows[i] for i in sorted(rows)]
|
||||||
|
else:
|
||||||
|
# fall back: one row of numbers per line
|
||||||
grid = []
|
grid = []
|
||||||
for line in text.splitlines():
|
for line in text.splitlines():
|
||||||
# strip brackets and split on commas/whitespace
|
|
||||||
nums = re.findall(r"-?\d+(?:\.\d+)?", line)
|
nums = re.findall(r"-?\d+(?:\.\d+)?", line)
|
||||||
if nums:
|
if nums:
|
||||||
grid.append([float(n) for n in nums])
|
grid.append([float(n) for n in nums])
|
||||||
|
|
||||||
if not grid:
|
if not grid:
|
||||||
raise ValueError("No numeric data found in input")
|
raise ValueError("No numeric data found in input")
|
||||||
width = len(grid[0])
|
width = len(grid[0])
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
//! Reads and stores every calibration constant the sensor needs: both the
|
||||||
|
//! trim-register values used to reproduce the factory calibration, and the
|
||||||
|
//! per-pixel compensation data used to turn raw ADC counts into a real
|
||||||
|
//! temperature (datasheet section 12).
|
||||||
|
|
||||||
|
use crate::eeprom::{read_f32, read_word};
|
||||||
|
use crate::Bus;
|
||||||
|
|
||||||
|
/// The sensor's pixel array is 16x16.
|
||||||
|
pub const GRID: usize = 16;
|
||||||
|
|
||||||
|
/// A full 16x16 per-pixel calibration grid (`ThGrad`, `ThOffset`, `Pij`).
|
||||||
|
pub type Grid16<T> = [[T; GRID]; GRID];
|
||||||
|
|
||||||
|
/// A per-row-profile calibration grid used for electrical offset and VDD
|
||||||
|
/// compensation. Only 8 row profiles are calibrated; each is reused for two
|
||||||
|
/// of the array's 16 physical rows, see [`crate::thermal::row_profile`].
|
||||||
|
pub type Grid8 = [[i16; GRID]; 8];
|
||||||
|
|
||||||
|
/// All calibration data read from the sensor's EEPROM at startup.
|
||||||
|
pub struct Calibration {
|
||||||
|
/// Trim register values used during factory calibration; must be
|
||||||
|
/// re-applied so the sensor's analog behaviour matches the calibration
|
||||||
|
/// data (datasheet section 11.3).
|
||||||
|
pub mbit: u8,
|
||||||
|
pub bias: u8,
|
||||||
|
pub clk: u8,
|
||||||
|
pub bpa: u8,
|
||||||
|
pub pu: u8,
|
||||||
|
|
||||||
|
/// Ambient temperature from PTAT (datasheet 12.1).
|
||||||
|
pub ptat_gradient: f32,
|
||||||
|
pub ptat_offset: f32,
|
||||||
|
|
||||||
|
/// Thermal offset compensation (datasheet 12.2).
|
||||||
|
pub gradient_scale_div: i32, // 2^gradScale
|
||||||
|
pub thermal_gradient: Grid16<i16>, // ThGrad
|
||||||
|
pub thermal_offset: Grid16<i16>, // ThOffset
|
||||||
|
|
||||||
|
/// VDD compensation (datasheet 12.4).
|
||||||
|
pub vdd_comp_gradient: Grid8,
|
||||||
|
pub vdd_comp_offset: Grid8,
|
||||||
|
pub vdd_scale_gradient_div: i32, // 2^VddScGrad
|
||||||
|
pub vdd_scale_offset_div: i32, // 2^VddScOff
|
||||||
|
pub vdd_at_calibration: [i32; 2], // VddTh1, VddTh2
|
||||||
|
pub ptat_at_calibration: [i32; 2], // PtatTh1, PtatTh2
|
||||||
|
|
||||||
|
/// Sensitivity / object temperature (datasheet 12.5).
|
||||||
|
pub pixc_min: f32,
|
||||||
|
pub pixc_max: f32,
|
||||||
|
pub epsilon: f32,
|
||||||
|
pub global_gain: f32,
|
||||||
|
pub sensitivity: Grid16<u16>, // Pij
|
||||||
|
|
||||||
|
/// Identifies which calibration table (see [`crate::lookup_table`])
|
||||||
|
/// this sensor was factory-calibrated against.
|
||||||
|
pub table_number: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Calibration {
|
||||||
|
/// Read the full calibration set from the sensor's EEPROM.
|
||||||
|
pub async fn read(i2c: &mut Bus) -> Self {
|
||||||
|
let mbit = read_word(i2c, 0x001A).await as u8;
|
||||||
|
let bias = read_word(i2c, 0x001B).await as u8;
|
||||||
|
let clk = read_word(i2c, 0x001C).await as u8;
|
||||||
|
let bpa = read_word(i2c, 0x001D).await as u8;
|
||||||
|
let pu = read_word(i2c, 0x001E).await as u8;
|
||||||
|
|
||||||
|
let ptat_gradient = read_f32(i2c, 0x0034, 0x0035).await;
|
||||||
|
let ptat_offset = read_f32(i2c, 0x0036, 0x0037).await;
|
||||||
|
|
||||||
|
let gradient_scale_div = 1i32 << read_word(i2c, 0x0008).await as u8;
|
||||||
|
let thermal_gradient = read_reversed_grid16_signed(i2c, 0x0100).await;
|
||||||
|
let thermal_offset = read_reversed_grid16_signed(i2c, 0x0200).await;
|
||||||
|
|
||||||
|
let vdd_comp_gradient = read_packed_grid8(i2c, 0x0040).await;
|
||||||
|
let vdd_comp_offset = read_packed_grid8(i2c, 0x00A0).await;
|
||||||
|
let vdd_scale_gradient_div = 1i32 << read_word(i2c, 0x003E).await as u8;
|
||||||
|
let vdd_scale_offset_div = 1i32 << read_word(i2c, 0x003F).await as u8;
|
||||||
|
let vdd_at_calibration = [
|
||||||
|
read_word(i2c, 0x0025).await as i32,
|
||||||
|
read_word(i2c, 0x0026).await as i32,
|
||||||
|
];
|
||||||
|
let ptat_at_calibration = [
|
||||||
|
read_word(i2c, 0x002C).await as i32,
|
||||||
|
read_word(i2c, 0x002D).await as i32,
|
||||||
|
];
|
||||||
|
|
||||||
|
let pixc_min = read_f32(i2c, 0x0000, 0x0001).await;
|
||||||
|
let pixc_max = read_f32(i2c, 0x0002, 0x0003).await;
|
||||||
|
let global_gain = read_word(i2c, 0x0009).await as f32;
|
||||||
|
let epsilon = read_word(i2c, 0x000D).await as f32;
|
||||||
|
let sensitivity = read_reversed_grid16_raw(i2c, 0x0300).await;
|
||||||
|
|
||||||
|
let table_number = read_word(i2c, 0x000C).await;
|
||||||
|
|
||||||
|
Calibration {
|
||||||
|
mbit,
|
||||||
|
bias,
|
||||||
|
clk,
|
||||||
|
bpa,
|
||||||
|
pu,
|
||||||
|
ptat_gradient,
|
||||||
|
ptat_offset,
|
||||||
|
gradient_scale_div,
|
||||||
|
thermal_gradient,
|
||||||
|
thermal_offset,
|
||||||
|
vdd_comp_gradient,
|
||||||
|
vdd_comp_offset,
|
||||||
|
vdd_scale_gradient_div,
|
||||||
|
vdd_scale_offset_div,
|
||||||
|
vdd_at_calibration,
|
||||||
|
ptat_at_calibration,
|
||||||
|
pixc_min,
|
||||||
|
pixc_max,
|
||||||
|
epsilon,
|
||||||
|
global_gain,
|
||||||
|
sensitivity,
|
||||||
|
table_number,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Datasheet Fig. 11 layout shared by `ThGrad`, `ThOffset` and `Pij`: 256
|
||||||
|
/// consecutive words, stored as two 8-row halves where the second half is
|
||||||
|
/// stored *row-reversed* (its first stored row is the array's last row).
|
||||||
|
async fn read_reversed_grid16_raw(i2c: &mut Bus, base: u16) -> Grid16<u16> {
|
||||||
|
let mut grid: Grid16<u16> = [[0u16; GRID]; GRID];
|
||||||
|
for row in 0..8 {
|
||||||
|
for col in 0..GRID {
|
||||||
|
grid[row][col] = read_word(i2c, base + (col + row * GRID) as u16).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for row in 0..8 {
|
||||||
|
for col in 0..GRID {
|
||||||
|
grid[GRID - 1 - row][col] = read_word(i2c, base + 128 + (col + row * GRID) as u16).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
grid
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`read_reversed_grid16_raw`], reinterpreted as signed values (`ThGrad`,
|
||||||
|
/// `ThOffset` are stored as 16-bit signed integers).
|
||||||
|
async fn read_reversed_grid16_signed(i2c: &mut Bus, base: u16) -> Grid16<i16> {
|
||||||
|
read_reversed_grid16_raw(i2c, base)
|
||||||
|
.await
|
||||||
|
.map(|row| row.map(|v| v as i16))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `VddCompGrad`/`VddCompOff` (datasheet 12.4): 128 values (8 row profiles x
|
||||||
|
/// 16 columns) packed as signed 12-bit values with an `0x800` bias, 4 values
|
||||||
|
/// per 3 EEPROM words.
|
||||||
|
async fn read_packed_grid8(i2c: &mut Bus, base: u16) -> Grid8 {
|
||||||
|
let mut grid: Grid8 = [[0i16; GRID]; 8];
|
||||||
|
let mut index = 0usize;
|
||||||
|
for group in 0..(GRID * 8 / 4) {
|
||||||
|
let w0 = read_word(i2c, base + (group * 3) as u16).await;
|
||||||
|
let w1 = read_word(i2c, base + (group * 3 + 1) as u16).await;
|
||||||
|
let w2 = read_word(i2c, base + (group * 3 + 2) as u16).await;
|
||||||
|
let packed_values = [
|
||||||
|
w0 & 0x0FFF,
|
||||||
|
(w0 >> 12) | ((w1 & 0x00FF) << 4),
|
||||||
|
(w1 >> 8) | ((w2 & 0x000F) << 8),
|
||||||
|
w2 >> 4,
|
||||||
|
];
|
||||||
|
for value in packed_values {
|
||||||
|
grid[index / GRID][index % GRID] = value as i16 - 0x800;
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
grid
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
//! Low-level helpers for reading the sensor's calibration EEPROM over I2C.
|
||||||
|
//!
|
||||||
|
//! The EEPROM is addressed in 16-bit words. Every read follows the same
|
||||||
|
//! read-only sequence described in the datasheet (section 11.4):
|
||||||
|
//! SET_ADDRESS -> NORMAL_READ -> GET_DATA -> ACTIVE. Nothing in this module
|
||||||
|
//! ever writes to the calibration area itself.
|
||||||
|
|
||||||
|
use crate::Bus;
|
||||||
|
|
||||||
|
const EEPROM_ADDR: u8 = 0x1B;
|
||||||
|
|
||||||
|
const SET_ADDRESS: u8 = 0x09;
|
||||||
|
const NORMAL_READ: u8 = 0x06;
|
||||||
|
const GET_DATA: u8 = 0x0B;
|
||||||
|
const ACTIVE: u8 = 0x01;
|
||||||
|
|
||||||
|
/// Read a single 16-bit word from the EEPROM at `addr`.
|
||||||
|
pub async fn read_word(i2c: &mut Bus, addr: u16) -> u16 {
|
||||||
|
i2c.write(EEPROM_ADDR, &[SET_ADDRESS, (addr >> 8) as u8, addr as u8])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
i2c.write(EEPROM_ADDR, &[NORMAL_READ]).await.unwrap();
|
||||||
|
let mut bytes = [0u8; 2];
|
||||||
|
i2c.write_read(EEPROM_ADDR, &[GET_DATA], &mut bytes).await.unwrap();
|
||||||
|
i2c.write(EEPROM_ADDR, &[ACTIVE]).await.unwrap();
|
||||||
|
u16::from_le_bytes(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reconstruct a little-endian `f32` stored across two consecutive EEPROM
|
||||||
|
/// words (low word first, matching how `PTATGrad`/`PTATOff`/`PixCmin`/
|
||||||
|
/// `PixCmax` etc. are stored).
|
||||||
|
pub async fn read_f32(i2c: &mut Bus, low_addr: u16, high_addr: u16) -> f32 {
|
||||||
|
let low = read_word(i2c, low_addr).await as u32;
|
||||||
|
let high = read_word(i2c, high_addr).await as u32;
|
||||||
|
f32::from_bits(high << 16 | low)
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
//! Generic bilinear-interpolation lookup against a Heimann "Table.c"-style
|
||||||
|
//! object-temperature calibration table (datasheet section 12.5).
|
||||||
|
//!
|
||||||
|
//! Heimann calibrates each sensor/optics/gain combination with its own
|
||||||
|
//! table, selected via a "table number" stored in the sensor's EEPROM. Only
|
||||||
|
//! tables using an equidistant signal axis (`EQUIADTABLE` in Heimann's
|
||||||
|
//! reference code) are supported here, which turns the row lookup into a
|
||||||
|
//! plain shift instead of a per-row search. All of the reference tables we
|
||||||
|
//! have for the 16x16 sensor use this layout.
|
||||||
|
//!
|
||||||
|
//! Both lookups below clamp out-of-range inputs to the nearest edge of the
|
||||||
|
//! table (linear extrapolation) instead of failing, so a pixel briefly
|
||||||
|
//! outside the calibrated range just gets a best-effort estimate rather than
|
||||||
|
//! an error.
|
||||||
|
|
||||||
|
/// The ambient-temperature column to use for a whole frame, located once via
|
||||||
|
/// [`LookupTable::locate_ambient_column`] since it doesn't vary pixel to
|
||||||
|
/// pixel.
|
||||||
|
pub struct AmbientColumn {
|
||||||
|
index: usize,
|
||||||
|
delta_dk: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LookupTable<const ROWS: usize, const COLS: usize> {
|
||||||
|
/// Heimann's table number (EEPROM `TN`) that this table was made for.
|
||||||
|
pub table_number: u16,
|
||||||
|
/// `values[row][column]` is the object temperature in deci-Kelvin.
|
||||||
|
/// `0` marks an entry outside the sensor's factory-calibrated range.
|
||||||
|
pub values: &'static [[u16; COLS]; ROWS],
|
||||||
|
/// Ambient temperature (deci-Kelvin) of each column.
|
||||||
|
pub ta_columns_dk: [u16; COLS],
|
||||||
|
/// Scale used to turn a sensitivity-compensated pixel signal into
|
||||||
|
/// "digits" comparable to this table (Heimann's `PCSCALEVAL`).
|
||||||
|
pub pixel_signal_scale: f32,
|
||||||
|
/// Signal digits between two adjacent rows (`ADEQUIDISTANCE`).
|
||||||
|
pub row_step_digits: i32,
|
||||||
|
/// `log2(row_step_digits)`; turns a signal into a row index with a shift
|
||||||
|
/// instead of a division (`ADEXPBITS`).
|
||||||
|
pub row_exp_bits: u32,
|
||||||
|
/// Added to a signal before locating its row, so that signals near zero
|
||||||
|
/// still map to a valid (non-negative) row index (`TABLEOFFSET`).
|
||||||
|
pub row_offset_digits: i32,
|
||||||
|
/// Deci-Kelvin between two adjacent columns (`TAEQUIDISTANCE`).
|
||||||
|
pub column_step_dk: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const ROWS: usize, const COLS: usize> LookupTable<ROWS, COLS> {
|
||||||
|
/// Locate the ambient-temperature column for `ambient_dk`, clamped to
|
||||||
|
/// the table's calibrated range.
|
||||||
|
pub fn locate_ambient_column(&self, ambient_dk: i32) -> AmbientColumn {
|
||||||
|
let last_selectable_column = COLS - 2; // `index + 1` must stay in bounds
|
||||||
|
let index = (0..=last_selectable_column)
|
||||||
|
.rev()
|
||||||
|
.find(|&i| self.ta_columns_dk[i] as i32 <= ambient_dk)
|
||||||
|
.unwrap_or(0);
|
||||||
|
AmbientColumn {
|
||||||
|
index,
|
||||||
|
delta_dk: ambient_dk - self.ta_columns_dk[index] as i32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bi-linear interpolation of the object temperature (deci-Kelvin) for
|
||||||
|
/// one pixel's sensitivity-compensated signal (in digits).
|
||||||
|
pub fn lookup(&self, column: &AmbientColumn, signal_digits: i32) -> i32 {
|
||||||
|
let shifted_signal = (signal_digits + self.row_offset_digits) >> self.row_exp_bits;
|
||||||
|
let row = shifted_signal.clamp(0, (ROWS - 2) as i32) as usize;
|
||||||
|
|
||||||
|
let c = column.index;
|
||||||
|
let top_left = self.values[row][c] as i32;
|
||||||
|
let top_right = self.values[row][c + 1] as i32;
|
||||||
|
let bottom_left = self.values[row + 1][c] as i32;
|
||||||
|
let bottom_right = self.values[row + 1][c + 1] as i32;
|
||||||
|
|
||||||
|
// interpolate along the ambient-temperature axis, at this row and the next
|
||||||
|
let at_row = (top_right - top_left) * column.delta_dk / self.column_step_dk + top_left;
|
||||||
|
let at_next_row =
|
||||||
|
(bottom_right - bottom_left) * column.delta_dk / self.column_step_dk + bottom_left;
|
||||||
|
|
||||||
|
// interpolate along the signal axis, between those two results
|
||||||
|
let row_start_digits = row as i32 * self.row_step_digits;
|
||||||
|
let position_in_row = (signal_digits + self.row_offset_digits) - row_start_digits;
|
||||||
|
(at_next_row - at_row) * position_in_row / self.row_step_digits + at_row
|
||||||
|
}
|
||||||
|
}
|
||||||
+62
-128
@@ -1,156 +1,90 @@
|
|||||||
#![no_main]
|
#![no_main]
|
||||||
#![no_std]
|
#![no_std]
|
||||||
|
|
||||||
|
mod calibration;
|
||||||
|
mod eeprom;
|
||||||
|
mod lookup_table;
|
||||||
|
mod sensor;
|
||||||
|
mod table_tn114;
|
||||||
|
mod thermal;
|
||||||
|
|
||||||
use defmt::*;
|
use defmt::*;
|
||||||
use embassy_executor::Spawner;
|
use embassy_executor::Spawner;
|
||||||
|
use embassy_stm32::dma;
|
||||||
use embassy_stm32::mode::Async;
|
use embassy_stm32::mode::Async;
|
||||||
use embassy_stm32::{Config, bind_interrupts, dma::InterruptHandler, i2c::{self, ErrorInterruptHandler, EventInterruptHandler, I2c, Master}, peripherals::{DMA1_CH1, DMA1_CH2, I2C2}};
|
use embassy_stm32::peripherals::{GPDMA1_CH0, GPDMA1_CH1, I2C2};
|
||||||
use embassy_time::Timer;
|
use embassy_stm32::{bind_interrupts, i2c, rcc, time, Config};
|
||||||
|
|
||||||
use defmt_rtt as _;
|
use defmt_rtt as _;
|
||||||
use panic_probe as _;
|
use panic_probe as _;
|
||||||
|
|
||||||
bind_interrupts!(struct Irqs {
|
bind_interrupts!(struct Irqs {
|
||||||
DMA1_CHANNEL2_3 => InterruptHandler<DMA1_CH2>;
|
GPDMA1_CHANNEL0 => dma::InterruptHandler<GPDMA1_CH0>;
|
||||||
DMA1_CHANNEL1 => InterruptHandler<DMA1_CH1>;
|
GPDMA1_CHANNEL1 => dma::InterruptHandler<GPDMA1_CH1>;
|
||||||
I2C2_3 => EventInterruptHandler<I2C2>, ErrorInterruptHandler<I2C2>;
|
I2C2_EV => i2c::EventInterruptHandler<I2C2>;
|
||||||
|
I2C2_ER => i2c::ErrorInterruptHandler<I2C2>;
|
||||||
});
|
});
|
||||||
|
|
||||||
const SENSOR: u8 = 0x1A;
|
/// Shared I2C bus type used by every module that talks to the sensor.
|
||||||
const EEPROM: u8 = 0x1B;
|
pub type Bus = i2c::I2c<'static, Async, i2c::Master>;
|
||||||
|
|
||||||
type Bus = I2c<'static, Async, Master>;
|
|
||||||
|
|
||||||
/// Read a 16-bit word from the sensor EEPROM (read-only sequence, never writes).
|
|
||||||
async fn eeprom_word(i2c: &mut Bus, addr: u16) -> u16 {
|
|
||||||
i2c.write(EEPROM, &[0x09, (addr >> 8) as u8, addr as u8]).await.unwrap(); // SET_ADDRESS
|
|
||||||
i2c.write(EEPROM, &[0x06]).await.unwrap(); // NORMAL_READ
|
|
||||||
let mut b = [0u8; 2];
|
|
||||||
i2c.write_read(EEPROM, &[0x0B], &mut b).await.unwrap(); // GET_DATA
|
|
||||||
i2c.write(EEPROM, &[0x01]).await.unwrap(); // ACTIVE
|
|
||||||
(b[1] as u16) << 8 | b[0] as u16
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reconstruct a little-endian float stored across two EEPROM words.
|
|
||||||
async fn eeprom_f32(i2c: &mut Bus, lo_addr: u16, hi_addr: u16) -> f32 {
|
|
||||||
let lo = eeprom_word(i2c, lo_addr).await as u32;
|
|
||||||
let hi = eeprom_word(i2c, hi_addr).await as u32;
|
|
||||||
f32::from_bits(hi << 16 | lo)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run one conversion with the given configuration byte and read both halves.
|
|
||||||
async fn read_block(i2c: &mut Bus, config: u8) -> ([u8; 130], [u8; 130]) {
|
|
||||||
i2c.write(SENSOR, &[0x01, config]).await.unwrap();
|
|
||||||
let mut status = [0u8; 1];
|
|
||||||
loop {
|
|
||||||
i2c.write_read(SENSOR, &[0x02], &mut status).await.unwrap();
|
|
||||||
if status[0] & 0x01 != 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut top = [0u8; 130];
|
|
||||||
let mut bottom = [0u8; 130];
|
|
||||||
i2c.write_read(SENSOR, &[0x0A], &mut top).await.unwrap();
|
|
||||||
i2c.write_read(SENSOR, &[0x0B], &mut bottom).await.unwrap();
|
|
||||||
(top, bottom)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pixel `j` (0..63) of a half-block buffer: word0 is PTAT/VDD, pixels start at byte 2.
|
|
||||||
fn px(buf: &[u8; 130], j: usize) -> u16 {
|
|
||||||
(buf[2 * j + 2] as u16) << 8 | buf[2 * j + 3] as u16
|
|
||||||
}
|
|
||||||
|
|
||||||
#[embassy_executor::main]
|
#[embassy_executor::main]
|
||||||
async fn main(_spawner: Spawner) {
|
async fn main(_spawner: Spawner) {
|
||||||
let p = embassy_stm32::init(Config::default());
|
let mut config = Config::default();
|
||||||
|
configure_clocks(&mut config);
|
||||||
|
let p = embassy_stm32::init(config);
|
||||||
|
|
||||||
let mut i2c_cfg = i2c::Config::default();
|
let mut i2c_cfg = i2c::Config::default();
|
||||||
i2c_cfg.sda_pullup = false;
|
i2c_cfg.sda_pullup = false;
|
||||||
i2c_cfg.scl_pullup = false;
|
i2c_cfg.scl_pullup = false;
|
||||||
let mut i2c = I2c::new(p.I2C2, p.PA7, p.PA6, p.DMA1_CH1, p.DMA1_CH2, Irqs, i2c_cfg);
|
let mut i2c = i2c::I2c::new(p.I2C2, p.PB13, p.PB14, p.GPDMA1_CH0, p.GPDMA1_CH1, Irqs, i2c_cfg);
|
||||||
|
|
||||||
// --- read calibration data from EEPROM (read-only) ---
|
let cal = calibration::Calibration::read(&mut i2c).await;
|
||||||
let mbit = eeprom_word(&mut i2c, 0x001A).await as u8;
|
info!("sensor table number (TN): {}", cal.table_number);
|
||||||
let bias = eeprom_word(&mut i2c, 0x001B).await as u8;
|
if cal.table_number != table_tn114::TABLE.table_number {
|
||||||
let clk = eeprom_word(&mut i2c, 0x001C).await as u8;
|
warn!(
|
||||||
let bpa = eeprom_word(&mut i2c, 0x001D).await as u8;
|
"sensor's TableNumber ({}) does not match the embedded calibration table ({}); \
|
||||||
let pu = eeprom_word(&mut i2c, 0x001E).await as u8;
|
object temperatures will be inaccurate",
|
||||||
let gradscale = eeprom_word(&mut i2c, 0x0008).await as u8;
|
cal.table_number,
|
||||||
let ptatgr = eeprom_f32(&mut i2c, 0x0034, 0x0035).await;
|
table_tn114::TABLE.table_number
|
||||||
let ptatoff = eeprom_f32(&mut i2c, 0x0036, 0x0037).await;
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ThGrad and ThOffset (16x16 signed), bottom half is stored row-reversed.
|
sensor::wake_up_and_load_trim(&mut i2c, &cal).await;
|
||||||
let mut thgrad = [[0i16; 16]; 16];
|
|
||||||
let mut thoffset = [[0i16; 16]; 16];
|
let frame = sensor::Frame::capture(&mut i2c).await;
|
||||||
for (base, dst) in [(0x0100u16, &mut thgrad), (0x0200u16, &mut thoffset)] {
|
let heatmap = thermal::process_frame(&cal, &frame, &table_tn114::TABLE);
|
||||||
for m in 0..8 {
|
|
||||||
for n in 0..16 {
|
info!("ambient temperature: {} C", heatmap.ambient_c);
|
||||||
dst[m][n] = eeprom_word(&mut i2c, base + (n + m * 16) as u16).await as i16;
|
info!("heatmap range: {}..{} C", heatmap.min_c, heatmap.max_c);
|
||||||
}
|
for row in 0..calibration::GRID {
|
||||||
}
|
info!("row {}: {}", row, heatmap.temperature_c[row]);
|
||||||
for k in 0..8 {
|
|
||||||
for n in 0..16 {
|
|
||||||
dst[15 - k][n] = eeprom_word(&mut i2c, base + 128 + (n + k * 16) as u16).await as i16;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- wake up sensor and load calibration into trim registers ---
|
/// Configure the system clock to run at its maximum from the board's 25 MHz
|
||||||
i2c.write(SENSOR, &[0x01, 0x01]).await.unwrap();
|
/// HSE: PLL1 = (25 MHz / 5) * 64 / 2 = 160 MHz, used as SYSCLK. Also enables
|
||||||
Timer::after_millis(30).await;
|
/// the board's 32.768 kHz LSE for the RTC.
|
||||||
for (reg, val) in [(0x03, mbit), (0x04, bias), (0x05, bias), (0x06, clk), (0x07, bpa), (0x08, bpa), (0x09, pu)] {
|
fn configure_clocks(config: &mut Config) {
|
||||||
i2c.write(SENSOR, &[reg, val]).await.unwrap();
|
config.rcc.hse = Some(rcc::Hse {
|
||||||
Timer::after_millis(5).await;
|
freq: time::Hertz(25_000_000),
|
||||||
}
|
mode: rcc::HseMode::Oscillator,
|
||||||
|
});
|
||||||
|
|
||||||
// --- read both pixel blocks (each carries a PTAT value + its pixels) ---
|
config.rcc.pll1 = Some(rcc::Pll {
|
||||||
let (top0, bot0) = read_block(&mut i2c, 0x09).await; // block 0, PTAT
|
source: rcc::PllSource::HSE,
|
||||||
let (top1, bot1) = read_block(&mut i2c, 0x19).await; // block 1, PTAT
|
prediv: rcc::PllPreDiv::DIV5,
|
||||||
// --- electrical offset (blind measurement) ---
|
mul: rcc::PllMul::MUL64,
|
||||||
let (eo_top, eo_bot) = read_block(&mut i2c, 0x0B).await;
|
divp: Some(rcc::PllDiv::DIV2),
|
||||||
|
divq: Some(rcc::PllDiv::DIV2),
|
||||||
|
divr: Some(rcc::PllDiv::DIV2), // 160 MHz
|
||||||
|
});
|
||||||
|
config.rcc.sys = rcc::Sysclk::PLL1_R;
|
||||||
|
|
||||||
// ambient temperature from averaged PTAT (datasheet 11.1)
|
config.rcc.ls.lse = Some(rcc::LseConfig {
|
||||||
let ptat = |b: &[u8; 130]| (b[0] as u32) << 8 | b[1] as u32;
|
frequency: time::Hertz(32_768),
|
||||||
let ptat_av = ((ptat(&top0) + ptat(&top1) + ptat(&bot0) + ptat(&bot1)) / 4) as f32;
|
mode: rcc::LseMode::Oscillator(rcc::LseDrive::MediumHigh),
|
||||||
let ambient_dk = ptat_av * ptatgr + ptatoff;
|
peripherals_clocked: false,
|
||||||
let ambient_c = ambient_dk / 10.0 - 273.15;
|
});
|
||||||
|
config.rcc.ls.rtc = rcc::RtcClockSource::LSE;
|
||||||
// sort raw pixels into a 16x16 image (datasheet ordering)
|
|
||||||
let mut image = [[0i32; 16]; 16];
|
|
||||||
let gradscale_div = 1i32 << gradscale;
|
|
||||||
for n in 0..16 {
|
|
||||||
let raw = [
|
|
||||||
px(&top0, n), px(&top0, n + 16), px(&top0, n + 32), px(&top0, n + 48),
|
|
||||||
px(&top1, n), px(&top1, n + 16), px(&top1, n + 32), px(&top1, n + 48),
|
|
||||||
px(&bot1, n + 48), px(&bot1, n + 32), px(&bot1, n + 16), px(&bot1, n),
|
|
||||||
px(&bot0, n + 48), px(&bot0, n + 32), px(&bot0, n + 16), px(&bot0, n),
|
|
||||||
];
|
|
||||||
let eo = [
|
|
||||||
px(&eo_top, n), px(&eo_top, n + 16), px(&eo_top, n + 32), px(&eo_top, n + 48),
|
|
||||||
px(&eo_bot, n + 48), px(&eo_bot, n + 32), px(&eo_bot, n + 16), px(&eo_bot, n),
|
|
||||||
];
|
|
||||||
for m in 0..16 {
|
|
||||||
// thermal-offset compensation (11.2) + electrical-offset compensation (11.3)
|
|
||||||
let comp = raw[m] as i32
|
|
||||||
- (thgrad[m][n] as i32 * ptat_av as i32) / gradscale_div
|
|
||||||
- thoffset[m][n] as i32;
|
|
||||||
let eo_row = if m < 8 { m % 4 } else { m % 4 + 4 };
|
|
||||||
image[m][n] = comp - eo[eo_row] as i32;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut min = i32::MAX;
|
|
||||||
let mut max = i32::MIN;
|
|
||||||
for row in &image {
|
|
||||||
for &v in row {
|
|
||||||
min = min.min(v);
|
|
||||||
max = max.max(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("ambient temperature: {} C (PTAT_av={})", ambient_c, ptat_av);
|
|
||||||
info!("compensated image range: {}..{}", min, max);
|
|
||||||
for m in 0..16 {
|
|
||||||
info!("row {}: {}", m, image[m]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
//! Talks to the sensor's pixel array (as opposed to its calibration
|
||||||
|
//! EEPROM, see [`crate::eeprom`]): waking it up, loading its trim
|
||||||
|
//! registers, and reading raw pixel/PTAT/VDD data.
|
||||||
|
|
||||||
|
use embassy_time::Timer;
|
||||||
|
|
||||||
|
use crate::calibration::Calibration;
|
||||||
|
use crate::Bus;
|
||||||
|
|
||||||
|
const SENSOR_ADDR: u8 = 0x1A;
|
||||||
|
|
||||||
|
const CONFIGURATION_REGISTER: u8 = 0x01;
|
||||||
|
const STATUS_REGISTER: u8 = 0x02;
|
||||||
|
const TOP_HALF: u8 = 0x0A;
|
||||||
|
const BOTTOM_HALF: u8 = 0x0B;
|
||||||
|
const END_OF_CONVERSION: u8 = 0x01;
|
||||||
|
|
||||||
|
/// Configuration register bits (datasheet Table 6).
|
||||||
|
mod config_bit {
|
||||||
|
pub const WAKEUP: u8 = 1 << 0;
|
||||||
|
pub const BLIND: u8 = 1 << 1; // sample electrical offsets instead of the pixels
|
||||||
|
pub const VDD_MEAS: u8 = 1 << 2; // measure VDD instead of PTAT
|
||||||
|
pub const START: u8 = 1 << 3;
|
||||||
|
pub const BLOCK1: u8 = 1 << 4; // select block 1 instead of block 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One raw half-array conversion result: a 2-byte header word (PTAT or VDD,
|
||||||
|
/// depending on which config bits were used) followed by 64 pixel words.
|
||||||
|
pub type RawHalf = [u8; 130];
|
||||||
|
|
||||||
|
/// Wake the sensor up and load the trim registers used during its factory
|
||||||
|
/// calibration (datasheet section 11.3). Must be called before capturing any
|
||||||
|
/// frames.
|
||||||
|
pub async fn wake_up_and_load_trim(i2c: &mut Bus, cal: &Calibration) {
|
||||||
|
i2c.write(SENSOR_ADDR, &[CONFIGURATION_REGISTER, config_bit::WAKEUP])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
Timer::after_millis(30).await;
|
||||||
|
|
||||||
|
let trim_registers = [
|
||||||
|
(0x03, cal.mbit),
|
||||||
|
(0x04, cal.bias),
|
||||||
|
(0x05, cal.bias),
|
||||||
|
(0x06, cal.clk),
|
||||||
|
(0x07, cal.bpa),
|
||||||
|
(0x08, cal.bpa),
|
||||||
|
(0x09, cal.pu),
|
||||||
|
];
|
||||||
|
for (register, value) in trim_registers {
|
||||||
|
i2c.write(SENSOR_ADDR, &[register, value]).await.unwrap();
|
||||||
|
Timer::after_millis(5).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Everything read from the sensor for one temperature calculation: the two
|
||||||
|
/// pixel/PTAT blocks, the two VDD blocks, and the electrical-offset (blind)
|
||||||
|
/// block.
|
||||||
|
pub struct Frame {
|
||||||
|
pub pixel_top: [RawHalf; 2],
|
||||||
|
pub pixel_bottom: [RawHalf; 2],
|
||||||
|
pub electrical_offset: (RawHalf, RawHalf),
|
||||||
|
pub vdd_top: [RawHalf; 2],
|
||||||
|
pub vdd_bottom: [RawHalf; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Frame {
|
||||||
|
/// Trigger the five conversions needed for one temperature calculation
|
||||||
|
/// and read all of their results back.
|
||||||
|
pub async fn capture(i2c: &mut Bus) -> Self {
|
||||||
|
use config_bit::*;
|
||||||
|
|
||||||
|
let (top0, bottom0) = read_halves(i2c, WAKEUP | START).await;
|
||||||
|
let (top1, bottom1) = read_halves(i2c, WAKEUP | START | BLOCK1).await;
|
||||||
|
let electrical_offset = read_halves(i2c, WAKEUP | START | BLIND).await;
|
||||||
|
let (vdd_top0, vdd_bottom0) = read_halves(i2c, WAKEUP | START | VDD_MEAS).await;
|
||||||
|
let (vdd_top1, vdd_bottom1) = read_halves(i2c, WAKEUP | START | VDD_MEAS | BLOCK1).await;
|
||||||
|
|
||||||
|
Frame {
|
||||||
|
pixel_top: [top0, top1],
|
||||||
|
pixel_bottom: [bottom0, bottom1],
|
||||||
|
electrical_offset,
|
||||||
|
vdd_top: [vdd_top0, vdd_top1],
|
||||||
|
vdd_bottom: [vdd_bottom0, vdd_bottom1],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trigger one conversion with the given configuration bits and read back
|
||||||
|
/// both halves of the array once it completes.
|
||||||
|
async fn read_halves(i2c: &mut Bus, config: u8) -> (RawHalf, RawHalf) {
|
||||||
|
i2c.write(SENSOR_ADDR, &[CONFIGURATION_REGISTER, config]).await.unwrap();
|
||||||
|
|
||||||
|
let mut status = [0u8; 1];
|
||||||
|
loop {
|
||||||
|
i2c.write_read(SENSOR_ADDR, &[STATUS_REGISTER], &mut status).await.unwrap();
|
||||||
|
if status[0] & END_OF_CONVERSION != 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut top = [0u8; 130];
|
||||||
|
let mut bottom = [0u8; 130];
|
||||||
|
i2c.write_read(SENSOR_ADDR, &[TOP_HALF], &mut top).await.unwrap();
|
||||||
|
i2c.write_read(SENSOR_ADDR, &[BOTTOM_HALF], &mut bottom).await.unwrap();
|
||||||
|
(top, bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Header word (bytes 0/1) of a half-array buffer: PTAT or VDD, depending on
|
||||||
|
/// which config bits were used for the conversion that produced it.
|
||||||
|
pub fn header_word(buf: &RawHalf) -> u32 {
|
||||||
|
((buf[0] as u32) << 8) | buf[1] as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pixel `index` (0..63) of a half-array buffer.
|
||||||
|
pub fn pixel(buf: &RawHalf, index: usize) -> u16 {
|
||||||
|
(buf[2 * index + 2] as u16) << 8 | buf[2 * index + 3] as u16
|
||||||
|
}
|
||||||
+1625
File diff suppressed because it is too large
Load Diff
+171
@@ -0,0 +1,171 @@
|
|||||||
|
//! Turns one raw sensor [`Frame`] plus [`Calibration`] data into a 16x16
|
||||||
|
//! object-temperature heatmap, following the compensation pipeline described
|
||||||
|
//! in the HTPA16x16dR2 datasheet, section 12:
|
||||||
|
//!
|
||||||
|
//! 1. ambient temperature, from PTAT (12.1)
|
||||||
|
//! 2. thermal offset compensation (12.2)
|
||||||
|
//! 3. electrical offset compensation (12.3)
|
||||||
|
//! 4. VDD compensation (12.4)
|
||||||
|
//! 5. sensitivity (PixC) compensation + lookup table (12.5)
|
||||||
|
|
||||||
|
use crate::calibration::{Calibration, GRID};
|
||||||
|
use crate::lookup_table::LookupTable;
|
||||||
|
use crate::sensor::{header_word, pixel, Frame, RawHalf};
|
||||||
|
|
||||||
|
/// The computed heatmap for one frame, plus a couple of handy summary stats.
|
||||||
|
pub struct Heatmap {
|
||||||
|
pub ambient_c: f32,
|
||||||
|
pub min_c: f32,
|
||||||
|
pub max_c: f32,
|
||||||
|
pub temperature_c: [[f32; GRID]; GRID],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compensate one raw frame into a full object-temperature heatmap.
|
||||||
|
pub fn process_frame<const ROWS: usize, const COLS: usize>(
|
||||||
|
cal: &Calibration,
|
||||||
|
frame: &Frame,
|
||||||
|
table: &LookupTable<ROWS, COLS>,
|
||||||
|
) -> Heatmap {
|
||||||
|
let ptat_av = average_header_word(&frame.pixel_top, &frame.pixel_bottom);
|
||||||
|
let vdd_av = average_header_word(&frame.vdd_top, &frame.vdd_bottom);
|
||||||
|
|
||||||
|
let ambient_dk = ptat_av as f32 * cal.ptat_gradient + cal.ptat_offset;
|
||||||
|
let ambient_c = dk_to_c(ambient_dk);
|
||||||
|
let ambient_column = table.locate_ambient_column(ambient_dk as i32);
|
||||||
|
|
||||||
|
let mut temperature_c = [[0f32; GRID]; GRID];
|
||||||
|
let mut min_c = f32::MAX;
|
||||||
|
let mut max_c = f32::MIN;
|
||||||
|
|
||||||
|
for col in 0..GRID {
|
||||||
|
let raw_pixels = assemble_raw_column(frame, col);
|
||||||
|
let electrical_offsets = electrical_offset_column(&frame.electrical_offset, col);
|
||||||
|
|
||||||
|
for row in 0..GRID {
|
||||||
|
let profile = row_profile(row);
|
||||||
|
|
||||||
|
let thermal_compensated =
|
||||||
|
raw_pixels[row] - thermal_offset_correction(cal, row, col, ptat_av);
|
||||||
|
let electrical_compensated =
|
||||||
|
thermal_compensated - electrical_offsets[profile] as i32;
|
||||||
|
let vdd_compensated = electrical_compensated
|
||||||
|
- vdd_compensation(cal, profile, col, ptat_av, vdd_av);
|
||||||
|
|
||||||
|
let pixc = sensitivity_coefficient(cal, row, col);
|
||||||
|
let signal_digits = (vdd_compensated as f32 * table.pixel_signal_scale / pixc) as i32;
|
||||||
|
|
||||||
|
let object_dk = table.lookup(&ambient_column, signal_digits);
|
||||||
|
let object_c = dk_to_c(object_dk as f32);
|
||||||
|
|
||||||
|
temperature_c[row][col] = object_c;
|
||||||
|
min_c = min_c.min(object_c);
|
||||||
|
max_c = max_c.max(object_c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Heatmap { ambient_c, min_c, max_c, temperature_c }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert deci-Kelvin (as used throughout the datasheet's calibration data)
|
||||||
|
/// to degrees Celsius.
|
||||||
|
fn dk_to_c(dk: f32) -> f32 {
|
||||||
|
dk / 10.0 - 273.15
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which of the 8 stored calibration row-profiles a physical row uses.
|
||||||
|
/// Electrical offset and VDD compensation are only calibrated for 8 rows;
|
||||||
|
/// each profile is reused for two of the array's 16 physical rows (the
|
||||||
|
/// bottom half is wired up in reverse row order, see the datasheet's
|
||||||
|
/// "Readout Order" figure).
|
||||||
|
fn row_profile(row: usize) -> usize {
|
||||||
|
if row < 8 {
|
||||||
|
row % 4
|
||||||
|
} else {
|
||||||
|
row % 4 + 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Average of the header word (PTAT or VDD) across a block-0/block-1 pair of
|
||||||
|
/// top/bottom half buffers (datasheet 12.1/12.4 both recommend averaging all
|
||||||
|
/// four samples for a more stable reading).
|
||||||
|
fn average_header_word(top: &[RawHalf; 2], bottom: &[RawHalf; 2]) -> i32 {
|
||||||
|
let sum = header_word(&top[0]) + header_word(&top[1]) + header_word(&bottom[0]) + header_word(&bottom[1]);
|
||||||
|
(sum / 4) as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reconstruct one column (16 rows) of raw pixel digits from the pixel
|
||||||
|
/// blocks, in the physical row order described in the datasheet's "Readout
|
||||||
|
/// Order" figure: block 0 and block 1 each contribute 8 rows, with the
|
||||||
|
/// bottom half read out column-major and row-reversed.
|
||||||
|
fn assemble_raw_column(frame: &Frame, col: usize) -> [i32; GRID] {
|
||||||
|
let top0 = &frame.pixel_top[0];
|
||||||
|
let top1 = &frame.pixel_top[1];
|
||||||
|
let bottom1 = &frame.pixel_bottom[1];
|
||||||
|
let bottom0 = &frame.pixel_bottom[0];
|
||||||
|
|
||||||
|
[
|
||||||
|
pixel(top0, col) as i32,
|
||||||
|
pixel(top0, col + 16) as i32,
|
||||||
|
pixel(top0, col + 32) as i32,
|
||||||
|
pixel(top0, col + 48) as i32,
|
||||||
|
pixel(top1, col) as i32,
|
||||||
|
pixel(top1, col + 16) as i32,
|
||||||
|
pixel(top1, col + 32) as i32,
|
||||||
|
pixel(top1, col + 48) as i32,
|
||||||
|
pixel(bottom1, col + 48) as i32,
|
||||||
|
pixel(bottom1, col + 32) as i32,
|
||||||
|
pixel(bottom1, col + 16) as i32,
|
||||||
|
pixel(bottom1, col) as i32,
|
||||||
|
pixel(bottom0, col + 48) as i32,
|
||||||
|
pixel(bottom0, col + 32) as i32,
|
||||||
|
pixel(bottom0, col + 16) as i32,
|
||||||
|
pixel(bottom0, col) as i32,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One column of the 8 electrical-offset row profiles (datasheet 12.3).
|
||||||
|
fn electrical_offset_column(halves: &(RawHalf, RawHalf), col: usize) -> [u16; 8] {
|
||||||
|
let (top, bottom) = halves;
|
||||||
|
[
|
||||||
|
pixel(top, col),
|
||||||
|
pixel(top, col + 16),
|
||||||
|
pixel(top, col + 32),
|
||||||
|
pixel(top, col + 48),
|
||||||
|
pixel(bottom, col + 48),
|
||||||
|
pixel(bottom, col + 32),
|
||||||
|
pixel(bottom, col + 16),
|
||||||
|
pixel(bottom, col),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Thermal offset correction to subtract from one pixel (datasheet 12.2):
|
||||||
|
/// `ThGrad * Ta / 2^gradScale + ThOffset`.
|
||||||
|
fn thermal_offset_correction(cal: &Calibration, row: usize, col: usize, ptat_av: i32) -> i32 {
|
||||||
|
(cal.thermal_gradient[row][col] as i32 * ptat_av) / cal.gradient_scale_div
|
||||||
|
+ cal.thermal_offset[row][col] as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// VDD compensation to subtract from one pixel (datasheet 12.4). Compares
|
||||||
|
/// the measured VDD against what it should be at the current ambient
|
||||||
|
/// temperature (interpolated between the two calibration points), scaled by
|
||||||
|
/// this pixel's VDD sensitivity.
|
||||||
|
fn vdd_compensation(cal: &Calibration, profile: usize, col: usize, ptat_av: i32, vdd_av: i32) -> i32 {
|
||||||
|
let gradient = cal.vdd_comp_gradient[profile][col] as i32;
|
||||||
|
let offset = cal.vdd_comp_offset[profile][col] as i32;
|
||||||
|
|
||||||
|
let [vdd_at_cal1, vdd_at_cal2] = cal.vdd_at_calibration;
|
||||||
|
let [ptat_at_cal1, ptat_at_cal2] = cal.ptat_at_calibration;
|
||||||
|
let expected_vdd = vdd_at_cal1
|
||||||
|
+ (vdd_at_cal2 - vdd_at_cal1) * (ptat_av - ptat_at_cal1) / (ptat_at_cal2 - ptat_at_cal1);
|
||||||
|
let vdd_delta = vdd_av - expected_vdd;
|
||||||
|
|
||||||
|
let scaled_gradient = gradient * ptat_av / cal.vdd_scale_gradient_div + offset;
|
||||||
|
scaled_gradient * vdd_delta / cal.vdd_scale_offset_div
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-pixel sensitivity coefficient, PixC (datasheet 12.5):
|
||||||
|
/// `(Pij * (PixCmax - PixCmin) / 65535 + PixCmin) * epsilon/100 * GlobalGain/10000`.
|
||||||
|
fn sensitivity_coefficient(cal: &Calibration, row: usize, col: usize) -> f32 {
|
||||||
|
let scaled = cal.sensitivity[row][col] as f32 * (cal.pixc_max - cal.pixc_min) / 65535.0 + cal.pixc_min;
|
||||||
|
scaled * (cal.epsilon / 100.0) * (cal.global_gain / 10000.0)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user