use core::convert::Infallible;
use embedded_graphics::pixelcolor::RgbColor;
use embedded_graphics::prelude::{DrawTarget, OriginDimensions, Point, Size};
use crate::Color;
use crate::{map_row_index, slot_addresses};
use crate::{BcmLenReps, FrameBuffer, BCM_SEQUENCE_CAPACITY};
use crate::{FrameBufferOperations, MutableFrameBuffer};
#[cfg(feature = "tail-closes-latch")]
use super::ADDR_MASK;
use super::{
make_data_template, map_index, Entry, ADDR_SHIFT, COLOR_MASK, INTER_ROW_BLANK, OE_BLANK,
};
#[derive(Clone, Copy, PartialEq, Debug)]
#[repr(C)]
pub struct Row<const COLS: usize> {
pub(crate) data: [Entry; COLS],
pub(crate) gap: [Entry; INTER_ROW_BLANK],
}
impl<const COLS: usize> Row<COLS> {
#[must_use]
pub const fn new() -> Self {
Self {
data: [Entry::new(); COLS],
gap: [Entry::new(); INTER_ROW_BLANK],
}
}
#[inline]
pub const fn format(&mut self, prev_addr: u8) {
let template = make_data_template::<COLS>(prev_addr);
let mut i = 0;
while i < COLS {
self.data[i] = template[i];
i += 1;
}
let gap_val = ((prev_addr as u16) << ADDR_SHIFT) | OE_BLANK;
let mut i = 0;
#[allow(clippy::absurd_extreme_comparisons)]
while i < INTER_ROW_BLANK {
self.gap[i].0 = gap_val;
i += 1;
}
}
}
impl<const COLS: usize> Default for Row<COLS> {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
#[repr(C)]
pub struct PlaneData<const NROWS: usize, const COLS: usize> {
pub(crate) rows: [Row<COLS>; NROWS],
#[cfg(all(feature = "tail-closes-latch", feature = "esp32-ordering"))]
pub(crate) padding: Entry,
#[cfg(feature = "tail-closes-latch")]
pub(crate) tail: Entry,
#[cfg(all(not(feature = "esp32-ordering"), feature = "tail-closes-latch"))]
pub(crate) padding: Entry,
}
impl<const NROWS: usize, const COLS: usize> PlaneData<NROWS, COLS> {
const fn new() -> Self {
Self {
rows: [Row::new(); NROWS],
#[cfg(all(feature = "esp32-ordering", feature = "tail-closes-latch"))]
padding: Entry::new(),
#[cfg(feature = "tail-closes-latch")]
tail: Entry::new(),
#[cfg(all(not(feature = "esp32-ordering"), feature = "tail-closes-latch"))]
padding: Entry::new(),
}
}
}
const fn plane_suffix(plane_idx: usize, planes: usize) -> (usize, usize) {
let covered = planes - plane_idx;
let reps = if plane_idx == 0 {
1
} else {
1 << (plane_idx - 1)
};
(covered, reps)
}
const fn bcm_sequence<const NROWS: usize, const COLS: usize, const PLANES: usize>(
) -> [BcmLenReps; BCM_SEQUENCE_CAPACITY] {
assert!(PLANES <= BCM_SEQUENCE_CAPACITY);
let plane_bytes = core::mem::size_of::<PlaneData<NROWS, COLS>>();
let mut seq = [BcmLenReps::ZERO; BCM_SEQUENCE_CAPACITY];
let mut plane = 0usize;
while plane < PLANES {
let (covered, reps) = plane_suffix(plane, PLANES);
seq[plane] = BcmLenReps {
len: plane_bytes * covered,
reps,
};
plane += 1;
}
seq
}
#[derive(Copy, Clone)]
#[repr(C, align(4))]
pub struct DmaFrameBuffer<const NROWS: usize, const COLS: usize, const PLANES: usize> {
pub(crate) planes: [PlaneData<NROWS, COLS>; PLANES],
}
impl<const NROWS: usize, const COLS: usize, const PLANES: usize>
DmaFrameBuffer<NROWS, COLS, PLANES>
{
#[must_use]
pub const fn new() -> Self {
assert!(NROWS >= 1 && NROWS <= 32, "NROWS must be within 1..=32");
assert!(PLANES >= 1 && PLANES <= 8, "PLANES must be within 1..=8");
#[cfg(feature = "esp32-ordering")]
assert!(
COLS % 2 == 0,
"esp32-ordering feature requires COLS to be even"
);
let mut instance = Self {
planes: [PlaneData::new(); PLANES],
};
instance.format();
instance
}
#[must_use]
#[deprecated(since = "0.11.0", note = "use BCM_SEQUENCE instead")]
pub const fn bcm_chunk_count() -> usize {
PLANES
}
#[must_use]
#[deprecated(since = "0.11.0", note = "use BCM_SEQUENCE instead")]
pub const fn bcm_chunk_bytes() -> usize {
core::mem::size_of::<PlaneData<NROWS, COLS>>()
}
#[inline]
pub const fn format(&mut self) {
let mut p = 0;
while p < PLANES {
let mut slot = 0;
while slot < NROWS {
let (prev_addr, _) = slot_addresses::<NROWS>(slot);
self.planes[p].rows[slot].format(prev_addr);
slot += 1;
}
#[cfg(feature = "tail-closes-latch")]
{
self.planes[p].tail.0 = ADDR_MASK | OE_BLANK;
self.planes[p].padding.0 = ADDR_MASK | OE_BLANK;
}
p += 1;
}
}
#[inline]
pub fn erase(&mut self) {
const MASK: u16 = !COLOR_MASK; for plane in &mut self.planes {
for row in &mut plane.rows {
for entry in &mut row.data {
entry.0 &= MASK;
}
}
}
}
#[inline]
pub fn set_pixel(&mut self, p: Point, color: Color) {
if p.x < 0 || p.y < 0 {
return;
}
self.set_pixel_internal(p.x as usize, p.y as usize, color);
}
#[inline]
fn set_pixel_internal(&mut self, x: usize, y: usize, color: Color) {
if x >= COLS || y >= NROWS * 2 {
return;
}
#[cfg(feature = "skip-black-pixels")]
if color == Color::BLACK {
return;
}
let row_idx = map_row_index::<NROWS>(if y < NROWS { y } else { y - NROWS });
let is_top = y < NROWS;
let red = color.r();
let green = color.g();
let blue = color.b();
for plane_idx in 0..PLANES {
let bit = (8 - PLANES + plane_idx) as u32;
let bits = ((u8::from(((blue >> bit) & 1) != 0)) << 2)
| ((u8::from(((green >> bit) & 1) != 0)) << 1)
| u8::from(((red >> bit) & 1) != 0);
let col_idx = map_index(x);
let entry = &mut self.planes[plane_idx].rows[row_idx].data[col_idx];
if is_top {
entry.set_color0_bits(bits);
} else {
entry.set_color1_bits(bits);
}
}
}
}
impl<const NROWS: usize, const COLS: usize, const PLANES: usize> Default
for DmaFrameBuffer<NROWS, COLS, PLANES>
{
fn default() -> Self {
Self::new()
}
}
impl<const NROWS: usize, const COLS: usize, const PLANES: usize> core::fmt::Debug
for DmaFrameBuffer<NROWS, COLS, PLANES>
{
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("DmaFrameBuffer")
.field("size", &core::mem::size_of_val(&self.planes))
.field("plane_count", &self.planes.len())
.field("plane_size", &core::mem::size_of_val(&self.planes[0]))
.finish()
}
}
#[cfg(feature = "defmt")]
impl<const NROWS: usize, const COLS: usize, const PLANES: usize> defmt::Format
for DmaFrameBuffer<NROWS, COLS, PLANES>
{
fn format(&self, f: defmt::Formatter) {
defmt::write!(f, "DmaFrameBuffer<{}, {}, {}>", NROWS, COLS, PLANES);
defmt::write!(f, " size: {}", core::mem::size_of_val(&self.planes));
defmt::write!(
f,
" plane_size: {}",
core::mem::size_of_val(&self.planes[0])
);
}
}
impl<const NROWS: usize, const COLS: usize, const PLANES: usize> FrameBuffer
for DmaFrameBuffer<NROWS, COLS, PLANES>
{
type Word = u16;
const BCM_SEQUENCE: [BcmLenReps; BCM_SEQUENCE_CAPACITY] = bcm_sequence::<NROWS, COLS, PLANES>();
const BCM_SEQUENCE_LEN: usize = PLANES;
const BCM_SEQUENCE_COUNT: usize = 1;
fn bcm_segment_ptr(&self, index: usize) -> *const u8 {
assert!(
index < PLANES,
"segment index {index} out of range for {PLANES} planes"
);
(&raw const self.planes[index]).cast::<u8>()
}
}
impl<const NROWS: usize, const COLS: usize, const PLANES: usize> FrameBufferOperations
for DmaFrameBuffer<NROWS, COLS, PLANES>
{
#[inline]
fn erase(&mut self) {
DmaFrameBuffer::<NROWS, COLS, PLANES>::erase(self);
}
#[inline]
fn set_pixel(&mut self, p: Point, color: Color) {
DmaFrameBuffer::<NROWS, COLS, PLANES>::set_pixel(self, p, color);
}
}
impl<const NROWS: usize, const COLS: usize, const PLANES: usize> MutableFrameBuffer
for DmaFrameBuffer<NROWS, COLS, PLANES>
{
}
impl<const NROWS: usize, const COLS: usize, const PLANES: usize> OriginDimensions
for DmaFrameBuffer<NROWS, COLS, PLANES>
{
fn size(&self) -> Size {
Size::new(COLS as u32, (NROWS * 2) as u32)
}
}
impl<const NROWS: usize, const COLS: usize, const PLANES: usize> DrawTarget
for DmaFrameBuffer<NROWS, COLS, PLANES>
{
type Color = Color;
type Error = Infallible;
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = embedded_graphics::Pixel<Self::Color>>,
{
for pixel in pixels {
if pixel.0.x < 0 || pixel.0.y < 0 {
continue;
}
self.set_pixel_internal(pixel.0.x as usize, pixel.0.y as usize, pixel.1);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
extern crate std;
use super::super::{LEAD_BLANK_DELAY, TRAIL_BLANK_DELAY};
use super::*;
use embedded_graphics::prelude::*;
use std::format;
const TEST_COLS: usize = if LEAD_BLANK_DELAY + TRAIL_BLANK_DELAY + 2 > 64 {
128
} else {
64
};
type TestBuffer = DmaFrameBuffer<16, TEST_COLS, 8>;
#[test]
fn row_format_sets_address_and_control_bits() {
let mut row = Row::<TEST_COLS>::new();
row.format(4);
let last_idx = map_index(TEST_COLS - 1);
assert_eq!(row.data[last_idx].latch(), true);
assert_eq!(row.data[last_idx].addr(), 4);
let first_idx = map_index(0);
assert_eq!(row.data[first_idx].addr(), 4);
assert_eq!(row.data[first_idx].latch(), false);
}
#[test]
fn format_sets_expected_row_addresses_for_all_rows() {
let mut fb = TestBuffer::new();
fb.format();
for plane_idx in 0..8 {
for slot in 0..16 {
let (prev_addr, _) = slot_addresses::<16>(slot);
let last_col = map_index(TEST_COLS - 1);
assert_eq!(
fb.planes[plane_idx].rows[slot].data[last_col].addr(),
u16::from(prev_addr)
);
assert_eq!(fb.planes[plane_idx].rows[slot].data[last_col].latch(), true);
}
}
}
#[test]
fn set_pixel_maps_top_half_bits_per_plane() {
let mut fb = TestBuffer::new();
let color = Color::new(0b1010_0101, 0b0101_1010, 0b1111_0000);
fb.set_pixel(Point::new(2, 3), color);
for plane_idx in 0..8 {
let bit = plane_idx;
let entry = fb.planes[plane_idx].rows[map_row_index::<16>(3)].data[map_index(2)];
assert_eq!(entry.red1(), ((color.r() >> bit) & 1) != 0);
assert_eq!(entry.grn1(), ((color.g() >> bit) & 1) != 0);
assert_eq!(entry.blu1(), ((color.b() >> bit) & 1) != 0);
}
}
#[test]
fn set_pixel_maps_bottom_half_bits_per_plane() {
let mut fb = TestBuffer::new();
let color = Color::new(0b1100_0011, 0b0011_1100, 0b1001_0110);
fb.set_pixel(Point::new(4, 20), color);
for plane_idx in 0..8 {
let bit = plane_idx;
let entry = fb.planes[plane_idx].rows[map_row_index::<16>(4)].data[map_index(4)];
assert_eq!(entry.red2(), ((color.r() >> bit) & 1) != 0);
assert_eq!(entry.grn2(), ((color.g() >> bit) & 1) != 0);
assert_eq!(entry.blu2(), ((color.b() >> bit) & 1) != 0);
}
}
#[test]
#[cfg(feature = "reverse-row-order")]
fn reverse_row_order_stores_rows_back_to_front() {
let mut fb = TestBuffer::new();
assert_eq!(fb.planes[0].rows[0].data[map_index(0)].addr(), 0);
assert_eq!(fb.planes[0].rows[15].data[map_index(0)].addr(), 1);
fb.set_pixel(Point::new(2, 0), Color::RED);
let col2 = map_index(2);
assert!(fb.planes[7].rows[15].data[col2].red1());
assert!(!fb.planes[7].rows[0].data[col2].red1());
}
#[test]
fn erase_clears_only_color_bits() {
let mut fb = TestBuffer::new();
let oe_before =
fb.planes[0].rows[map_row_index::<16>(0)].data[map_index(1)].output_enable();
fb.set_pixel(Point::new(0, 0), Color::WHITE);
fb.erase();
for plane in &fb.planes {
for row in &plane.rows {
for entry in &row.data {
assert!(!entry.red1());
assert!(!entry.grn1());
assert!(!entry.blu1());
assert!(!entry.red2());
assert!(!entry.grn2());
assert!(!entry.blu2());
}
}
}
assert_eq!(
fb.planes[0].rows[map_row_index::<16>(0)].data[map_index(1)].output_enable(),
oe_before
);
}
#[test]
fn draw_target_iter_sets_pixels() {
let mut fb = TestBuffer::new();
let pixels = [Pixel(Point::new(1, 1), Color::RED)];
let result = fb.draw_iter(pixels);
assert!(result.is_ok());
for plane_idx in 0..8 {
let bit = plane_idx;
let entry = fb.planes[plane_idx].rows[map_row_index::<16>(1)].data[map_index(1)];
assert_eq!(entry.red1(), ((Color::RED.r() >> bit) & 1) != 0);
assert!(!entry.grn1());
assert!(!entry.blu1());
}
}
#[test]
fn set_pixel_ignores_out_of_bounds_and_negative() {
let mut fb = TestBuffer::new();
let before = fb.planes;
fb.set_pixel(Point::new(-1, 0), Color::WHITE);
fb.set_pixel(Point::new(0, -1), Color::WHITE);
fb.set_pixel(Point::new(TEST_COLS as i32, 0), Color::WHITE);
fb.set_pixel(Point::new(0, 32), Color::WHITE);
assert_eq!(fb.planes, before);
}
#[test]
#[cfg(feature = "skip-black-pixels")]
fn test_skip_black_pixels_enabled() {
let mut fb = TestBuffer::new();
fb.set_pixel_internal(10, 5, Color::RED);
let mapped_col_10 = map_index(10);
assert!(fb.planes[0].rows[map_row_index::<16>(5)].data[mapped_col_10].red1());
assert!(!fb.planes[0].rows[map_row_index::<16>(5)].data[mapped_col_10].grn1());
assert!(!fb.planes[0].rows[map_row_index::<16>(5)].data[mapped_col_10].blu1());
fb.set_pixel_internal(10, 5, Color::BLACK);
assert!(fb.planes[0].rows[map_row_index::<16>(5)].data[mapped_col_10].red1());
assert!(!fb.planes[0].rows[map_row_index::<16>(5)].data[mapped_col_10].grn1());
assert!(!fb.planes[0].rows[map_row_index::<16>(5)].data[mapped_col_10].blu1());
}
#[test]
#[cfg(not(feature = "skip-black-pixels"))]
fn test_skip_black_pixels_disabled() {
let mut fb = TestBuffer::new();
fb.set_pixel_internal(10, 5, Color::RED);
let mapped_col_10 = map_index(10);
assert!(fb.planes[0].rows[map_row_index::<16>(5)].data[mapped_col_10].red1());
assert!(!fb.planes[0].rows[map_row_index::<16>(5)].data[mapped_col_10].grn1());
assert!(!fb.planes[0].rows[map_row_index::<16>(5)].data[mapped_col_10].blu1());
fb.set_pixel_internal(10, 5, Color::BLACK);
assert!(!fb.planes[0].rows[map_row_index::<16>(5)].data[mapped_col_10].red1());
assert!(!fb.planes[0].rows[map_row_index::<16>(5)].data[mapped_col_10].grn1());
assert!(!fb.planes[0].rows[map_row_index::<16>(5)].data[mapped_col_10].blu1());
}
#[test]
#[should_panic(expected = "NROWS must be within 1..=32")]
fn new_panics_for_too_many_rows() {
let _ = DmaFrameBuffer::<33, TEST_COLS, 8>::new();
}
#[test]
#[should_panic(expected = "PLANES must be within 1..=8")]
fn new_panics_for_too_many_planes() {
let _ = DmaFrameBuffer::<16, TEST_COLS, 9>::new();
}
#[test]
#[allow(deprecated)]
fn bcm_chunk_info_for_common_panel() {
assert_eq!(TestBuffer::bcm_chunk_count(), 8);
assert_eq!(
TestBuffer::bcm_chunk_bytes(),
core::mem::size_of::<PlaneData<16, TEST_COLS>>()
);
}
#[test]
fn origin_dimensions_match_panel_geometry() {
let fb = TestBuffer::new();
assert_eq!(fb.size(), Size::new(TEST_COLS as u32, 32));
}
#[test]
fn debug_impl_includes_shape_information() {
let fb = TestBuffer::new();
let s = format!("{fb:?}");
assert!(s.contains("DmaFrameBuffer"));
assert!(s.contains("plane_count"));
assert!(s.contains("plane_size"));
}
#[test]
fn row_format_sets_expected_blank_and_latch_positions() {
let mut row = Row::<TEST_COLS>::new();
row.format(4);
let oe_active = cfg!(feature = "invert-oe");
let idx_active = map_index(TRAIL_BLANK_DELAY);
assert_eq!(row.data[idx_active].output_enable(), oe_active);
assert_eq!(row.data[idx_active].addr(), 4);
let idx_blank = map_index(TEST_COLS - LEAD_BLANK_DELAY - 1);
assert_eq!(row.data[idx_blank].output_enable(), !oe_active);
let idx_last = map_index(TEST_COLS - 1);
assert!(row.data[idx_last].latch());
assert_eq!(row.data[idx_last].addr(), 4);
}
#[test]
fn default_constructors_match_new() {
let row_default = Row::<TEST_COLS>::default();
let row_new = Row::<TEST_COLS>::new();
assert_eq!(row_default, row_new);
let fb_default = TestBuffer::default();
let fb_new = TestBuffer::new();
assert_eq!(fb_default.planes, fb_new.planes);
}
#[test]
fn framebuffer_operations_trait_delegates_correctly() {
let mut fb = TestBuffer::new();
FrameBufferOperations::set_pixel(&mut fb, Point::new(3, 5), Color::GREEN);
assert!(fb.planes[0].rows[map_row_index::<16>(5)].data[map_index(3)].grn1());
FrameBufferOperations::erase(&mut fb);
for plane in &fb.planes {
for row in &plane.rows {
for entry in &row.data {
assert!(!entry.red1());
assert!(!entry.grn1());
assert!(!entry.blu1());
assert!(!entry.red2());
assert!(!entry.grn2());
assert!(!entry.blu2());
}
}
}
}
#[test]
fn entry_debug_shows_hex_value() {
let mut entry = Entry::new();
entry.set_red1(true);
let s = format!("{entry:?}");
assert!(s.contains("Entry"));
assert!(s.contains("0x"));
}
#[test]
fn entry_new_oe_matches_feature() {
let entry = Entry::new();
assert_eq!(entry.output_enable(), !cfg!(feature = "invert-oe"));
}
#[test]
fn make_data_template_oe_polarity() {
let mut row = Row::<TEST_COLS>::new();
row.format(4);
let oe_active = cfg!(feature = "invert-oe");
let active_idx = map_index(TRAIL_BLANK_DELAY);
let blank_idx = map_index(TEST_COLS - LEAD_BLANK_DELAY - 1);
let latch_idx = map_index(TEST_COLS - 1);
assert_eq!(row.data[active_idx].output_enable(), oe_active);
assert_eq!(row.data[blank_idx].output_enable(), !oe_active);
assert_eq!(row.data[latch_idx].output_enable(), !oe_active);
}
static STATIC_FB: TestBuffer = TestBuffer::new();
#[test]
fn test_static_construction_is_formatted() {
let runtime_fb = TestBuffer::new();
for (pi, plane) in STATIC_FB.planes.iter().enumerate() {
for (ri, row) in plane.rows.iter().enumerate() {
assert_eq!(
row.data, runtime_fb.planes[pi].rows[ri].data,
"static vs runtime mismatch at plane {pi}, row {ri}"
);
assert_eq!(
row.gap, runtime_fb.planes[pi].rows[ri].gap,
"static vs runtime gap mismatch at plane {pi}, row {ri}"
);
}
}
}
#[test]
fn test_format_reinitializes_at_runtime() {
let mut fb = TestBuffer::new();
fb.erase();
fb.format();
for (pi, plane) in fb.planes.iter().enumerate() {
for (ri, row) in plane.rows.iter().enumerate() {
assert_eq!(
row.data, STATIC_FB.planes[pi].rows[ri].data,
"re-formatted vs static mismatch at plane {pi}, row {ri}"
);
}
}
}
#[test]
fn bcm_segment_count_equals_planes() {
let fb = TestBuffer::new();
assert_eq!(fb.bcm_segment_count(), 8);
}
#[test]
fn bcm_segments_per_group_is_one() {
use crate::FrameBuffer;
let fb = TestBuffer::new();
assert_eq!(fb.bcm_segments_per_group(), 1);
}
#[test]
fn bcm_segments_have_correct_reps() {
use crate::FrameBuffer;
let fb = TestBuffer::new();
for i in 0..8 {
let seg = fb.bcm_segment(i);
let expected = if i == 0 { 1 } else { 1 << (i - 1) };
assert_eq!(seg.reps, expected, "wrong reps for segment {i}");
}
}
#[test]
fn bcm_segments_point_to_plane_data() {
use crate::FrameBuffer;
let fb = TestBuffer::new();
let plane_len = core::mem::size_of::<PlaneData<16, TEST_COLS>>();
for i in 0..8 {
let seg = fb.bcm_segment(i);
let expected_ptr = (&raw const fb.planes[i]).cast::<u8>();
assert_eq!(seg.ptr, expected_ptr, "wrong ptr for segment {i}");
assert_eq!(seg.len, plane_len * (8 - i), "wrong len for segment {i}");
}
}
#[test]
fn bcm_segment_total_reps_is_halved_by_coalescing() {
use crate::FrameBuffer;
let fb = TestBuffer::new();
let total: usize = (0..fb.bcm_segment_count())
.map(|i| fb.bcm_segment(i).reps)
.sum();
assert_eq!(total, 1 << (8 - 1));
}
#[test]
fn bcm_segment_plane_coverage_matches_bcm_weights() {
use crate::FrameBuffer;
let fb = TestBuffer::new();
let plane_bytes = core::mem::size_of::<PlaneData<16, TEST_COLS>>();
for plane in 0..8usize {
let mut coverage = 0;
for first in 0..=plane {
let seg = fb.bcm_segment(first);
if first + seg.len / plane_bytes > plane {
coverage += seg.reps;
}
}
assert_eq!(coverage, 1 << plane, "plane {plane} coverage wrong");
}
}
#[test]
#[should_panic(expected = "out of range")]
fn bcm_segment_panics_for_invalid_index() {
use crate::FrameBuffer;
let fb = TestBuffer::new();
let _ = fb.bcm_segment(8);
}
#[test]
fn bcm_sequence_matches_runtime_segments() {
use crate::FrameBuffer;
let fb = TestBuffer::new();
assert_eq!(
TestBuffer::BCM_SEGMENT_COUNT,
TestBuffer::BCM_SEQUENCE_LEN * TestBuffer::BCM_SEQUENCE_COUNT
);
assert_eq!(fb.bcm_segment_count(), TestBuffer::BCM_SEGMENT_COUNT);
assert_eq!(
fb.bcm_segments_per_group(),
TestBuffer::BCM_SEGMENTS_PER_GROUP
);
assert_eq!(
TestBuffer::BCM_SEQUENCE_LEN % TestBuffer::BCM_SEGMENTS_PER_GROUP,
0
);
for i in 0..TestBuffer::BCM_SEGMENT_COUNT {
let entry = TestBuffer::BCM_SEQUENCE[i % TestBuffer::BCM_SEQUENCE_LEN];
let seg = fb.bcm_segment(i);
assert_eq!((seg.len, seg.reps), (entry.len, entry.reps), "segment {i}");
assert!(!seg.ptr.is_null(), "segment {i} has null pointer");
}
for entry in &TestBuffer::BCM_SEQUENCE[TestBuffer::BCM_SEQUENCE_LEN..] {
assert_eq!(*entry, crate::BcmLenReps::ZERO, "padding must be zero");
}
}
}