//! DDC support
const std = @import("std");
const microzig = @import("microzig");
const pico = microzig.hal;
const peripherals = microzig.chip.peripherals;
// TODO: extended address byte support (2.3)
pub const Parser = struct {
const PREAMBLE_MASK = 0b11111111110;
pub const TIME_ONEMAX = 87 * 1000; // XXX: doble check these!
// zig fmt: off
const MASK_3BYTES = 0b1111111111_1_00000000_1_00000000_1_00000000_1;
const COMPARE_3BYTES = 0b1111111111_0_00000000_0_00000000_0_00000000_1;
const MASK_4BYTES = 0b1111111111_1_00000000_1_00000000_1_00000000_1_00000000_1;
const COMPARE_4BYTES = 0b1111111111_0_00000000_0_00000000_0_00000000_0_00000000_1;
const MASK_5BYTES = 0b1111111111_1_00000000_1_00000000_1_00000000_1_00000000_1_00000000_1;
const COMPARE_5BYTES = 0b1111111111_0_00000000_0_00000000_0_00000000_0_00000000_0_00000000_1;
// zig fmt: on
const Speed = enum(u5) { stop, stopi, estop, estopi, step1, step2, step3, step4, step5, step6, step7, step8, step9, step10, step11, step12, step13, step14, step15, step16, step17, step18, step19, step20, step21, step22, step23, step24, step25, step26, step27, step28 };
const Packet = struct {
address: u8,
instruction: Instruction,
pub fn speed(self: @This()) Speed {
// TODO: Based on CV29 we should add support for 14 step speed control
var ret: u5 = self.instruction.speed << 1;
ret |= self.instruction.headlight;
return @enumFromInt(ret);
}
};
const Direction = enum(u1) {
backward,
forward,
};
/// Defined in S-9.2.1, Chapter 2.3
const InstructionType = enum(u3) {
decoder_control,
advanced_operating,
speed_reverse,
speed_forward,
function_group1,
function_group2,
feature_expansion,
cv_access,
};
const Instruction = packed struct(u8) {
speed: u4,
headlight: u1, // lsb of speed
itype: InstructionType,
};
const ParserState = enum {
preamble,
package,
};
const BitType = enum(u1) {
zero,
one,
};
const BUFFER_TYPE = u32; // 10+1 + 5x(8+1)
state: ParserState = .preamble,
time: microzig.drivers.time.Absolute = @enumFromInt(0),
buffer: BUFFER_TYPE = 0,
packets: std.fifo.LinearFifo(Packet, .{ .Static = 8 }) = std.fifo.LinearFifo(Packet, .{ .Static = 8 }).init(),
pub fn listen(self: @This(), cv: *const CV) void {
// TODO: S-9.2: handle decoder idle and reset!
_ = self;
_ = cv;
}
pub fn signalRising(self: *@This()) void {
self.time = pico.time.get_time_since_boot();
}
pub fn signalFalling(self: *@This()) void {
const time_end = pico.time.get_time_since_boot();
const diff = time_end.diff(self.time);
const bit: BitType = if (diff.less_than(@enumFromInt(TIME_ONEMAX))) .one else .zero;
switch (self.state) {
.preamble => {
switch (bit) {
.one => {
self.writeToBuffer(bit);
},
.zero => {
self.writeToBuffer(bit);
std.log.debug("DDC buffer: {b}", .{self.buffer});
if (self.buffer & PREAMBLE_MASK == PREAMBLE_MASK) {
self.state = .package;
std.log.debug("DDC state switched to: {}", .{self.state});
} else {
std.log.info("DDC preamble is invalid: {b}", .{self.buffer});
self.buffer = 0;
}
},
}
},
.package => {
self.writeToBuffer(bit);
// std.log.debug("DDC buffer: {b}", .{ddc.buffer});
// check for valid DCC packets
if (self.buffer & @as(BUFFER_TYPE, @truncate(MASK_3BYTES)) ==
@as(BUFFER_TYPE, @truncate(COMPARE_3BYTES)))
{
std.log.debug("3 byte long DCC package detected!", .{});
defer self.reset();
const address: u8 = @truncate(self.buffer >> (2 * (8 + 1) + 1));
const instruction: u8 = @truncate(self.buffer >> (8 + 1) + 1);
const checksum: u8 = @truncate(self.buffer >> 1);
if (address ^ instruction == checksum) {
std.log.debug("DDC packet passed the validation", .{});
const packet = Packet{
.address = @bitCast(address),
.instruction = @bitCast(instruction),
};
self.packets.writeItem(packet) catch {
std.log.err("DDC packet FIFO full, dropping packet", .{});
return;
};
std.log.debug("DCC packet added to FIFO", .{});
} else {
std.log.debug("DDC packet invalid", .{});
}
}
},
}
}
fn writeToBuffer(self: *@This(), bit: BitType) void {
self.buffer <<= 1;
self.buffer |= @intFromEnum(bit);
}
fn reset(self: *@This()) void {
self.buffer = 0;
self.state = .preamble;
}
};
/// Helper functions for testing the DDC parser
pub const Test = struct {
pub fn sendOne() void {
peripherals.IO_BANK0.PROC0_INTF1.modify(.{ .GPIO15_EDGE_HIGH = 1 });
pico.time.sleep_us(Parser.TIME_ONEMAX / 2);
peripherals.IO_BANK0.PROC0_INTF1.modify(.{ .GPIO15_EDGE_LOW = 1 });
}
pub fn sendZero() void {
peripherals.IO_BANK0.PROC0_INTF1.modify(.{ .GPIO15_EDGE_HIGH = 1 });
pico.time.sleep_us(Parser.TIME_ONEMAX * 2);
peripherals.IO_BANK0.PROC0_INTF1.modify(.{ .GPIO15_EDGE_LOW = 1 });
}
pub fn sendByte(bits: u8) void {
for (0..8) |i| {
const msb = bits >> (7 - @as(u3, @intCast(i)));
if (@as(u1, @truncate(msb)) == 0) {
sendZero();
} else {
sendOne();
}
}
}
};
pub const CV = packed struct {
primary_address: u8,
vstart: u8,
acceleration_rate: u8 = 0,
deceleration_rate: u8 = 0,
vhigh: u8,
vmid: u8,
manufacturer_version: u8 = 0,
manufacturer_id: u8 = 13,
PWM_period: u8,
EMF_feedback_cutout: u8, // CV10
packet_timeout: u8,
power_source_conversion: u8,
AMFS_F1_F8: u8 = 0,
AMFS_FL_F9_F12: u8 = 0,
decoder_lock: u16 = 0,
extended_address: u16,
consist_address: u8 = 0,
NMRA_reserved20: u8 = 0, // CV20
consist_F1_F8: u8 = 0,
consist_FL_F9_F12: u8 = 0,
acceleration_adjustment: u8 = 0,
deceleration_adjustment: u8 = 0,
speed_table_select: u8 = 0, // TODO: not sure it worth the effort if speed PID is working...
NMRA_reserved26: u8 = 0,
decoder_automatic_stopping: u8 = 0, // TODO: disable setting these values if not implemented
bidir_comm_config: u8 = 0, // TODO: disable setting these values if not implemented
configuration_data: Configuration,
error_information: u8 = 0, // CV30
index_high_byte: u8,
index_low_byte: u8,
output_loc: u112 = 0, // 33..46 => 14*8=112
manufacturer_unique47_64: u144 = 0, // 47..64 => 18*8=144
kick_start: u8 = 0,
forward_trim: u8 = 0,
speed_table: u224 = 0, // 67..94 => 28*8=224
reverse_trim: u8 = 0,
NMRA_reserved96_104: u72 = 0, // 96..104 => 9*8=72
user_identifier1: u8 = 0,
user_identifier2: u8 = 0,
NMRA_reserved107_111: u40 = 0, // 107..111 => 5*8=40
manufacturer_unique112_256: u1160 = 0, // 112..256 => 145*8=1160
/// Normally the CV table is 1024*8=8192 bits, but this part is unimplemented
/// anyway, so we can just drop it for now for quarter size
// indexed_area: u2048 = 0, // 257..512 => 256*8=2048
// NMRA_reserved513_879: u2936 = 0, // 513..879 => 367*8=2936
// NMRA_reserved880_891: u96 = 0, // 880..891 => 12*8=96
// decoder_load: u8 = 0,
// dynamic_flags: u8 = 0,
// fuel_coal: u8 = 0,
// water: u8 = 0,
// SUSI: u1032 = 0, // 896..1024 => 129*8=1032
const Configuration = packed struct {
accessory_decoder: bool,
reserved: u1 = 0,
extended_addressing: bool,
speed_table: u1,
bidirectional_communication: bool,
power_source_conversion: bool,
FL_location: u1,
direction: u1,
};
pub const default: CV = .{
.primary_address = 3,
.vstart = 8,
.vhigh = 0, // not used yet
.vmid = 0, // not used yet
.PWM_period = 0b10010110,
.EMF_feedback_cutout = 1,
.packet_timeout = 0, // TODO: S-9.2.4 Section C recommends 20 seconds, not implemented yet
.power_source_conversion = 0, // TODO Go to analog mode when CV1==0
.extended_address = 0b11000011_11101000,
.index_high_byte = 0b00010000,
.index_low_byte = 0b00000000,
.configuration_data = .{
.accessory_decoder = false,
.extended_addressing = false,
.speed_table = 0, // vstart-vmid-vhigh
.bidirectional_communication = false,
.power_source_conversion = false,
.FL_location = 0, // TODO not implemented
.direction = 0, // TODO not implemented
},
};
/// Expects [0..1023], so for CV29 `cv` should be 28
/// XXX: We cut the CVs, so actually it is [0..255]
pub fn getCV(self: @This(), cv: u8) u8 {
std.debug.assert(@bitSizeOf(CV) == 2048);
std.debug.assert(@sizeOf(CV) == 256);
return @truncate(@as(u2048, @bitCast(self)) >> cv * 8);
}
pub fn validate(self: @This()) void {
std.debug.assert(self.primary_address < 128);
std.debug.assert(self.extended_address >> 8 >= 0b11000000 and self.extended_address >> 8 <= 0b11100111);
// 0b00000000..0b00001111 reserved by NMRA
std.debug.assert(self.index_high_byte > 0b00001111);
}
};