commit 5259c04eabc55e65a124f79b963d2f4e45d2f9e5
parent 2be6a19907a081b3489323b6bc3cb6cee478f169
Author: Sylvia Ivory <git@sivory.net>
Date: Thu, 12 Mar 2026 19:11:55 -0700
Merge branch 'features/spi'
Diffstat:
9 files changed, 1255 insertions(+), 2 deletions(-)
diff --git a/pi/devices/aux.zig b/pi/devices/aux.zig
@@ -0,0 +1,57 @@
+const std = @import("std");
+
+const mem = @import("../mem.zig");
+const register = @import("../register.zig");
+
+pub const BASE_ADDRESS: usize = mem.BASE_ADDRESS + 0x0021_5000;
+const AUX_IRQ: register.Register(Aux, .ReadOnly) = .init(BASE_ADDRESS + 0x00);
+const AUX_ENABLES: register.Register(Aux, .ReadModifyWrite) = .init(BASE_ADDRESS + 0x04);
+
+const Aux = packed struct(u32) {
+ mini_uart: bool,
+ spi_1: bool,
+ spi_2: bool,
+ _reserved_31_3: u29,
+};
+
+pub fn enable_mini_uart() void {
+ var aux = AUX_ENABLES.get();
+ aux.mini_uart = true;
+ AUX_ENABLES.set(aux);
+}
+pub fn disable_mini_uart() void {
+ var aux = AUX_ENABLES.get();
+ aux.mini_uart = false;
+ AUX_ENABLES.set(aux);
+}
+pub fn has_irq_mini_uart() bool {
+ return AUX_IRQ.get().mini_uart;
+}
+
+pub fn enable_spi_1() void {
+ var aux = AUX_ENABLES.get();
+ aux.spi_1 = true;
+ AUX_ENABLES.set(aux);
+}
+pub fn disable_spi_1() void {
+ var aux = AUX_ENABLES.get();
+ aux.spi_1 = false;
+ AUX_ENABLES.set(aux);
+}
+pub fn has_irq_spi_1() bool {
+ return AUX_IRQ.get().spi_1;
+}
+
+pub fn enable_spi_2() void {
+ var aux = AUX_ENABLES.get();
+ aux.spi_2 = true;
+ AUX_ENABLES.set(aux);
+}
+pub fn disable_spi_2() void {
+ var aux = AUX_ENABLES.get();
+ aux.spi_1 = false;
+ AUX_ENABLES.set(aux);
+}
+pub fn has_irq_spi_2() bool {
+ return AUX_IRQ.get().spi_2;
+}
diff --git a/pi/devices/mini-spi.zig b/pi/devices/mini-spi.zig
@@ -0,0 +1,240 @@
+const std = @import("std");
+
+const aux = @import("./aux.zig");
+const gpio = @import("./gpio.zig");
+const mem = @import("../mem.zig");
+const register = @import("../register.zig");
+
+const BASE_ADDRESS_1: usize = aux.BASE_ADDRESS + 0x80;
+const BASE_ADDRESS_2: usize = aux.BASE_ADDRESS + 0xC0;
+
+const AUX_SPI1_CNTL0: register.Register(Control0, .ReadModifyWrite) = .init(BASE_ADDRESS_1 + 0x00);
+const AUX_SPI1_CNTL1: register.Register(Control1, .ReadModifyWrite) = .init(BASE_ADDRESS_1 + 0x04);
+const AUX_SPI1_STAT: register.Register(Status, .ReadModifyWrite) = .init(BASE_ADDRESS_1 + 0x08);
+const AUX_SPI1_PEEK: register.Register(FiFo, .ReadOnly) = .init(BASE_ADDRESS_1 + 0x0C);
+const AUX_SPI1_IO: register.Register(FiFo, .ReadModifyWrite) = .init(BASE_ADDRESS_1 + 0x20);
+
+const AUX_SPI2_CNTL0: register.Register(Control0, .ReadModifyWrite) = .init(BASE_ADDRESS_2 + 0x00);
+const AUX_SPI2_CNTL1: register.Register(Control1, .ReadModifyWrite) = .init(BASE_ADDRESS_2 + 0x04);
+const AUX_SPI2_STAT: register.Register(Status, .ReadModifyWrite) = .init(BASE_ADDRESS_2 + 0x08);
+const AUX_SPI2_PEEK: register.Register(FiFo, .ReadOnly) = .init(BASE_ADDRESS_2 + 0x0C);
+const AUX_SPI2_IO: register.Register(FiFo, .ReadModifyWrite) = .init(BASE_ADDRESS_2 + 0x20);
+
+// Unlike UART, only 1 pair
+const SPI1_CE2 = 16;
+const SPI1_CE1 = 17;
+const SPI1_CE0 = 18;
+const SPI1_MISO = 19;
+const SPI1_MOSI = 20;
+const SPI1_SCLK = 21;
+
+const SPI2_CE2 = 40;
+const SPI2_CE1 = 41;
+const SPI2_CE0 = 42;
+const SPI2_MISO = 43;
+const SPI2_MOSI = 44;
+const SPI2_SCLK = 45;
+
+const Control0 = packed struct(u32) {
+ pub const DOutHold = enum(u2) {
+ None = 0b00,
+ @"1" = 0b01,
+ @"4" = 0b10,
+ @"7" = 0b11,
+ };
+ pub const ChipSelect = packed struct(u3) {
+ @"0": bool,
+ @"1": bool,
+ @"2": bool,
+ };
+ shift_length: u6,
+ shift_out_ms: bool,
+ invert_spi_clk: bool,
+ out_rising: bool,
+ clear_fifos: bool,
+ in_rising: bool,
+ enable: bool,
+ dout_hold: DOutHold,
+ variable_width: bool,
+ variable_cs: bool,
+ post_input: bool,
+ chip_select: ChipSelect,
+ speed: u12,
+};
+const Control1 = packed struct(u32) {
+ keep_input: bool,
+ shift_in_ms: bool,
+ _reserved_5_2: u4 = 0,
+ done_irq: bool,
+ tx_empty_irq: bool,
+ cs_high_time: u3,
+ _reserved_31_11: u21 = 0,
+};
+const Status = packed struct(u32) {
+ bit_count: u6,
+ busy: bool,
+ rx_empty: bool,
+ rx_full: bool,
+ tx_empty: bool,
+ tx_full: bool,
+ _reserved_15_11: u5 = 0,
+ rx_fill_level: u4,
+ tx_fill_level: u4,
+ _reserved_31_22: u8,
+};
+const FiFo = packed struct(u32) {
+ data: u16,
+ _reserved_16_31: u16 = 0,
+};
+
+pub const SpiDevice = enum {
+ SPI1,
+ SPI2,
+};
+
+pub const SpiConfig = struct {
+ speed: u12,
+ chip_select: Control0.ChipSelect = .{
+ .@"0" = false,
+ .@"1" = false,
+ .@"2" = false,
+ },
+ shift_length: u6 = 8,
+ out_rising: bool = true,
+ in_rising: bool = false,
+ invert_clk: bool = false,
+ shift_out_ms: bool = true,
+ shift_in_ms: bool = false,
+ cs_high_time: u3 = 0,
+};
+
+pub fn init(device: SpiDevice, config: SpiConfig) void {
+ const cntl0_reg = switch (device) {
+ .SPI1 => AUX_SPI1_CNTL0,
+ .SPI2 => AUX_SPI2_CNTL0,
+ };
+ const cntl1_reg = switch (device) {
+ .SPI1 => AUX_SPI1_CNTL1,
+ .SPI2 => AUX_SPI2_CNTL1,
+ };
+
+ switch (device) {
+ .SPI1 => {
+ gpio.fn_sel(SPI1_MISO, .alt_fn_4) catch {};
+ gpio.set_pull(SPI1_MISO, .Up) catch {};
+
+ gpio.fn_sel(SPI1_MOSI, .alt_fn_4) catch {};
+ gpio.set_pull(SPI1_MOSI, .Up) catch {};
+
+ gpio.fn_sel(SPI1_SCLK, .alt_fn_4) catch {};
+ if (config.invert_clk) {
+ gpio.set_pull(SPI1_SCLK, .Up) catch {};
+ } else {
+ gpio.set_pull(SPI1_SCLK, .Down) catch {};
+ }
+
+ gpio.fn_sel(SPI1_CE0, .alt_fn_4) catch {};
+ gpio.set_pull(SPI1_CE2, .Up) catch {};
+
+ gpio.fn_sel(SPI1_CE1, .alt_fn_4) catch {};
+ gpio.set_pull(SPI1_CE2, .Up) catch {};
+
+ gpio.fn_sel(SPI1_CE2, .alt_fn_4) catch {};
+ gpio.set_pull(SPI1_CE2, .Up) catch {};
+
+ aux.enable_spi_1();
+ },
+ .SPI2 => {
+ gpio.fn_sel(SPI2_MISO, .alt_fn_4) catch {};
+ gpio.set_pull(SPI2_MISO, .Up) catch {};
+
+ gpio.fn_sel(SPI2_MOSI, .alt_fn_4) catch {};
+ gpio.set_pull(SPI2_MOSI, .Up) catch {};
+
+ gpio.fn_sel(SPI2_SCLK, .alt_fn_4) catch {};
+ if (config.invert_clk) {
+ gpio.set_pull(SPI2_SCLK, .Up) catch {};
+ } else {
+ gpio.set_pull(SPI2_SCLK, .Down) catch {};
+ }
+
+ gpio.fn_sel(SPI2_CE0, .alt_fn_4) catch {};
+ gpio.set_pull(SPI2_CE2, .Up) catch {};
+
+ gpio.fn_sel(SPI2_CE1, .alt_fn_4) catch {};
+ gpio.set_pull(SPI2_CE1, .Up) catch {};
+
+ gpio.fn_sel(SPI2_CE2, .alt_fn_4) catch {};
+ gpio.set_pull(SPI2_CE2, .Up) catch {};
+
+ aux.enable_spi_2();
+ },
+ }
+
+ cntl0_reg.set(.{
+ .shift_length = config.shift_length,
+ .shift_out_ms = config.shift_out_ms,
+ .invert_spi_clk = config.invert_clk,
+ .out_rising = config.out_rising,
+ .clear_fifos = true,
+ .in_rising = config.in_rising,
+ .enable = true,
+ .dout_hold = .None,
+ .variable_width = false,
+ .variable_cs = false,
+ .post_input = false,
+ .chip_select = config.chip_select,
+ .speed = config.speed,
+ });
+
+ cntl1_reg.set(.{
+ .keep_input = false,
+ .shift_in_ms = config.shift_in_ms,
+ .done_irq = false,
+ .tx_empty_irq = false,
+ .cs_high_time = config.cs_high_time,
+ });
+}
+
+pub fn status(device: SpiDevice) Status {
+ return switch (device) {
+ .SPI1 => AUX_SPI1_STAT.get(),
+ .SPI2 => AUX_SPI2_STAT.get(),
+ };
+}
+
+pub fn write_polled(device: SpiDevice, data: []const u8) void {
+ const io_reg = switch (device) {
+ .SPI1 => AUX_SPI1_IO,
+ .SPI2 => AUX_SPI2_IO,
+ };
+
+ for (data) |b| {
+ while (status(device).tx_full) {}
+ io_reg.set(.{
+ .data = @as(u16, b),
+ });
+ }
+}
+
+pub fn read_polled(device: SpiDevice, data: []u8) void {
+ const io_reg = switch (device) {
+ .SPI1 => AUX_SPI1_IO,
+ .SPI2 => AUX_SPI2_IO,
+ };
+
+ for (0..data.len) |i| {
+ while (status(device).rx_empty) {}
+ data[i] = @truncate(io_reg.get().data);
+ }
+}
+
+pub fn transfer(device: SpiDevice, src: []const u8, dst: []u8) void {
+ write_polled(device, src);
+ flush(device);
+ read_polled(device, dst);
+}
+
+pub fn flush(device: SpiDevice) void {
+ while (status(device).busy) {}
+}
diff --git a/pi/devices/nrf.zig b/pi/devices/nrf.zig
@@ -0,0 +1,548 @@
+// nRF24L01+
+
+const std = @import("std");
+const spi = @import("./spi.zig");
+const gpio = @import("./gpio.zig");
+const clock = @import("./clock.zig");
+const uart = @import("./mini-uart.zig");
+
+// 51: 8.3.1 SPI commands
+const CMD_R_REGISTER: u8 = 0b0000_0000;
+const CMD_W_REGISTER: u8 = 0b0010_0000;
+const CMD_R_RX_PAYLOAD: u8 = 0b0110_0001;
+const CMD_W_TX_PAYLOAD: u8 = 0b1010_0000;
+const CMD_FLUSH_TX: u8 = 0b1110_0001;
+const CMD_FLUSH_RX: u8 = 0b1110_0010;
+const CMD_REUSE_TX_PL: u8 = 0b1110_0011;
+const CMD_R_RX_PL_WID: u8 = 0b0110_0000;
+const CMD_W_ACK_PAYLOAD: u8 = 0b1010_1000;
+const CMD_W_TX_PAYLOAD_NO_ACK: u8 = 0b1011_0000;
+const CMD_NOP: u8 = 0b1111_1111;
+
+// 57: 9.1 Register map table
+const REG_CONFIG: u5 = 0x00;
+const nRFConfig = packed struct(u8) {
+ const PrimRx = enum(u1) {
+ PRX = 1,
+ PTX = 0,
+ };
+ const Crc = enum(u1) {
+ @"1" = 0,
+ @"2" = 1,
+ };
+ prim_rx: PrimRx = .PTX,
+ pwr_up: bool = false,
+ crc: Crc = .@"1",
+ enable_crc: bool = true,
+ mask_max_rt: bool = false,
+ mask_tx_rs: bool = false,
+ mask_rx_dr: bool = false,
+ _reserved_7: u1 = 0,
+};
+
+// Pipes
+const Pipes = packed struct(u8) {
+ @"0": bool = true,
+ @"1": bool = true,
+ @"2": bool = true,
+ @"3": bool = true,
+ @"4": bool = true,
+ @"5": bool = true,
+ _reserved_7_6: u2 = 0,
+};
+const REG_EN_AA: u5 = 0x01;
+const REG_EN_RXADDR: u5 = 0x02;
+
+const REG_SETUP_AW: u5 = 0x03;
+const AddressWidths = packed struct(u8) {
+ const Widths = enum(u2) {
+ @"3" = 0b01,
+ @"4" = 0b10,
+ @"5" = 0b11,
+ };
+
+ width: Widths,
+ _reserved_7_2: u6 = 0,
+};
+
+const REG_SETUP_RETR: u5 = 0x04;
+const SetupRetry = packed struct(u8) {
+ delay: u4 = 0b0000,
+ attempts: u4 = 0b011,
+};
+
+const REG_RF_CH: u5 = 0x05;
+const RFChannel = packed struct(u8) {
+ channel: u7 = 0b0000010,
+ _reserved_7: u1 = 0,
+};
+
+const REG_RF_SETUP: u5 = 0x06;
+const RFSetup = packed struct(u8) {
+ const OutputPower = enum(u2) {
+ @"-18dBm" = 0b00,
+ @"-12dBm" = 0b01,
+ @"-6dBm" = 0b10,
+ @"0dBm" = 0b11,
+ };
+ const Speed = enum(u2) {
+ @"1Mbps" = 0b00,
+ @"2Mbps" = 0b01,
+ @"250kbps" = 0b10,
+ };
+
+ _reserved_0: u1 = 0,
+ output_power: OutputPower = .@"0dBm",
+ rf_high: bool = true,
+ pll_lock: bool = false,
+ rf_low: bool = false,
+ _reserved_6: u1 = 0,
+ continuous_transmission: bool = false,
+};
+
+const REG_STATUS: u5 = 0x07;
+const Status = packed struct(u8) {
+ tx_full: bool = false,
+ rx_pipe_number: u3 = 0b111,
+ maxed_retries: bool = false,
+ tx_ds: bool = false,
+ rx_dr: bool = false,
+ _reserved_7: u1 = 0,
+};
+
+const REG_OBSERVE_TX: u5 = 0x08;
+const ObserveTx = packed struct(u8) {
+ retransmit_count: u4 = 0,
+ lost_count: u4 = 0,
+};
+
+const REG_RPD: u5 = 0x09;
+const ReceivedPowerDetector = packed struct(u8) {
+ rpd: bool = false,
+ _reserved_7_1: u7,
+};
+
+const REG_RX_ADDR_P0: u5 = 0x0A;
+const REG_RX_ADDR_P1: u5 = 0x0B;
+const REG_RX_ADDR_P2: u5 = 0x0C;
+const REG_RX_ADDR_P3: u5 = 0x0D;
+const REG_RX_ADDR_P4: u5 = 0x0E;
+const REG_RX_ADDR_P5: u5 = 0x0F;
+const REG_TX_ADDR: u5 = 0x10;
+
+const RxPayload = packed struct(u8) {
+ bytes: u6 = 0,
+ _reserved_7_6: u2 = 0,
+};
+const REG_RX_PW_P0: u5 = 0x11;
+const REG_RX_PW_P1: u5 = 0x12;
+const REG_RX_PW_P2: u5 = 0x13;
+const REG_RX_PW_P3: u5 = 0x14;
+const REG_RX_PW_P4: u5 = 0x15;
+const REG_RX_PW_P5: u5 = 0x16;
+
+const REG_FIFO_STATUS: u5 = 0x17;
+const FiFoStatus = packed struct(u8) {
+ rx_empty: bool,
+ rx_full: bool,
+ _reserved_3_2: u2,
+ tx_empty: bool,
+ tx_full: bool,
+ tx_reuse: bool,
+ _reserved_7: u1 = 0,
+};
+
+// Pipes
+const REG_DYNPD: u5 = 0x1C;
+
+const REG_FEATURE: u5 = 0x1D;
+const Feature = packed struct(u8) {
+ enable_dynamic_acknowledge: bool = false,
+ enable_acknowledge_payload: bool = false,
+ enable_dynamic_payload_length: bool = false,
+ _reserved_7_3: u5 = 0,
+};
+
+// Implementation
+pub const Error = error{
+ InvalidPipe,
+ InvalidPayload,
+ InvalidPayloadSize,
+ NotReady,
+ PayloadTooLarge,
+ Timeout,
+};
+
+pub const Config = struct {
+ spi_config: spi.SpiConfig,
+ ce_pin: u8,
+ int_pin: u8,
+
+ channel: u7 = 2,
+ output_power: RFSetup.OutputPower = .@"0dBm",
+ data_rate: RFSetup.Speed = .@"1Mbps",
+ crc_length: nRFConfig.Crc = .@"2",
+ auto_retransmit_delay: u4 = 2, // 500us
+ auto_retransmit_count: u4 = 15,
+ payload_size: u6 = 32,
+};
+
+pub const Device = struct {
+ spi_config: spi.SpiConfig,
+ ce_pin: u8,
+ int_pin: u8,
+
+ const Self = @This();
+
+ pub fn init(config: Config) !Self {
+ var self: Self = .{
+ .spi_config = config.spi_config,
+ .ce_pin = config.ce_pin,
+ .int_pin = config.int_pin,
+ };
+
+ self.spi_config.chip_select_polarity = .ActiveLow;
+ self.spi_config.clock_polarity = .Low;
+ self.spi_config.clock_phase = .Middle;
+ spi.init(&self.spi_config, 64);
+
+ try gpio.fn_sel(config.ce_pin, .output);
+ try gpio.set_off(config.ce_pin);
+
+ // Power on reset 100ms
+ clock.delay_ms(100);
+
+ // Configure
+ self.write_register(nRFConfig, REG_CONFIG, .{
+ .pwr_up = false,
+ .crc = .@"2",
+ .prim_rx = .PTX,
+ });
+
+ // Setup Auto Acknowledge
+ self.write_register(Pipes, REG_EN_AA, .{
+ .@"0" = true,
+ .@"1" = true,
+ .@"2" = true,
+ .@"3" = true,
+ .@"4" = true,
+ .@"5" = true,
+ });
+
+ // Enable Pipes 0/1
+ self.write_register(Pipes, REG_EN_RXADDR, .{
+ .@"0" = true,
+ .@"1" = true,
+ .@"2" = false,
+ .@"3" = false,
+ .@"4" = false,
+ .@"5" = false,
+ });
+
+ // Set Address Width
+ self.write_register(AddressWidths, REG_SETUP_AW, .{
+ .width = .@"5",
+ });
+
+ // Setup retry
+ self.write_register(SetupRetry, REG_SETUP_RETR, .{
+ .attempts = config.auto_retransmit_count,
+ .delay = config.auto_retransmit_delay,
+ });
+
+ // Set channel
+ self.write_register(RFChannel, REG_RF_CH, .{
+ .channel = config.channel,
+ });
+
+ // Set data rate and power
+ self.set_rf(config.output_power, config.data_rate);
+
+ // Clear DYNPD & FEATURE
+ self.write_register(u8, REG_FEATURE, 0);
+ self.write_register(u8, REG_DYNPD, 0);
+
+ if (config.payload_size > 32) return Error.InvalidPayloadSize;
+ try self.set_rx_payload_size(0, config.payload_size);
+ try self.set_rx_payload_size(1, config.payload_size);
+
+ // Flush before power up
+ self.flush_tx();
+ self.flush_rx();
+ self.clear_irq();
+
+ // Power on (Standby-I)
+ self.power_up();
+
+ return self;
+ }
+
+ fn power_up(self: *const Self) void {
+ var cfg = self.read_register(nRFConfig, REG_CONFIG);
+ cfg.pwr_up = true;
+ self.write_register(nRFConfig, REG_CONFIG, cfg);
+ clock.delay_ms(2);
+ }
+
+ fn power_down(self: *const Self) void {
+ var cfg = self.read_register(nRFConfig, REG_CONFIG);
+ cfg.pwr_up = false;
+ self.write_register(nRFConfig, REG_CONFIG, cfg);
+ self.set_ce_low();
+ }
+
+ fn set_ce_high(self: *const Self) void {
+ gpio.set_on(self.ce_pin) catch {};
+ }
+
+ fn set_ce_low(self: *const Self) void {
+ gpio.set_off(self.ce_pin) catch {};
+ }
+
+ fn pulse_ce(self: *const Self) void {
+ self.set_ce_high();
+ clock.delay(15);
+ self.set_ce_low();
+ }
+
+ fn set_rf(self: *const Self, power: RFSetup.OutputPower, speed: RFSetup.Speed) void {
+ const rate: u2 = @intFromEnum(speed);
+ const low = (rate & 0b10) == 0b10;
+ const high = (rate & 0b01) == 0b01;
+
+ self.write_register(RFSetup, REG_RF_SETUP, .{
+ .output_power = power,
+ .rf_low = low,
+ .rf_high = high,
+ });
+ }
+
+ // u40 = 5 byte addresses
+ pub fn set_tx_addr(self: *const Self, addr: u40) void {
+ var tx_data: [5]u8 = undefined;
+ std.mem.writeInt(u40, &tx_data, addr, .little);
+ self.write_register_n(REG_TX_ADDR, &tx_data);
+ }
+
+ pub fn set_rx_addr(self: *const Self, pipe: u3, addr: u40) !void {
+ if (pipe > 5) return Error.InvalidPipe;
+
+ if (pipe <= 1) {
+ var tx_data: [5]u8 = undefined;
+ std.mem.writeInt(u40, &tx_data, addr, .little);
+ self.write_register_n(REG_RX_ADDR_P0 + pipe, &tx_data);
+ } else {
+ const tx_data = [_]u8{@truncate(addr)};
+ self.write_register_n(REG_RX_ADDR_P0 + pipe, &tx_data);
+ }
+ }
+
+ fn set_rx_payload_size(self: *const Self, pipe: u3, size: u6) Error!void {
+ if (size > 32) return Error.InvalidPayloadSize;
+ if (pipe > 5) return Error.InvalidPipe;
+
+ self.write_register(RxPayload, REG_RX_PW_P0 + pipe, .{
+ .bytes = size,
+ });
+ }
+
+ fn enable_dynamic_payloads(self: *const Self) void {
+ var feature = self.read_register(Feature, REG_FEATURE);
+ feature.enable_dynamic_payload_length = true;
+ self.write_register(Feature, REG_FEATURE, feature);
+ self.write_register(Pipes, REG_DYNPD, .{
+ .@"0" = true,
+ .@"1" = true,
+ .@"2" = true,
+ .@"3" = true,
+ .@"4" = true,
+ .@"5" = true,
+ });
+ }
+
+ fn disable_dynamic_payloads(self: *const Self) void {
+ var feature = self.read_register(Feature, REG_FEATURE);
+ feature.enable_dynamic_payload_length = false;
+ self.write_register(Feature, REG_FEATURE, feature);
+ self.write_register(Pipes, REG_DYNPD, .{
+ .@"0" = false,
+ .@"1" = false,
+ .@"2" = false,
+ .@"3" = false,
+ .@"4" = false,
+ .@"5" = false,
+ });
+ }
+
+ pub fn connect(self: *const Self) void {
+ var cfg = self.read_register(nRFConfig, REG_CONFIG);
+ cfg.prim_rx = .PRX;
+ self.write_register(nRFConfig, REG_CONFIG, cfg);
+
+ self.clear_irq();
+ self.flush_rx();
+
+ self.set_ce_high();
+ clock.delay(150);
+ }
+
+ pub fn disconnect(self: *const Self) void {
+ self.set_ce_low();
+ clock.delay(150);
+
+ var cfg = self.read_register(nRFConfig, REG_CONFIG);
+ cfg.prim_rx = .PTX;
+ self.write_register(nRFConfig, REG_CONFIG, cfg);
+
+ self.flush_tx();
+ }
+
+ pub fn transmit(self: *const Self, payload: []const u8, timeout_ms: u32) Error!bool {
+ if (payload.len == 0 or payload.len > 32) return Error.InvalidPayload;
+
+ self.disconnect();
+ self.clear_irq();
+ self.flush_tx();
+
+ self.write_command(CMD_W_TX_PAYLOAD, payload);
+
+ self.pulse_ce();
+
+ const deadline = clock.current_count() + std.time.us_per_ms * timeout_ms;
+ while (clock.current_count() < deadline) {
+ const status = self.get_status();
+
+ if (status.tx_ds) {
+ self.write_register(Status, REG_STATUS, .{
+ .tx_ds = true,
+ });
+
+ return true;
+ }
+
+ if (status.maxed_retries) {
+ self.write_register(Status, REG_STATUS, .{
+ .maxed_retries = true,
+ });
+ self.flush_tx();
+
+ return false;
+ }
+
+ clock.delay_ms(1);
+ }
+
+ return Error.Timeout;
+ }
+
+ pub fn receive(self: *const Self, buffer: []u8) Error!usize {
+ const fifo = self.read_register(FiFoStatus, REG_FIFO_STATUS);
+ if (fifo.rx_empty) return 0;
+
+ var width: usize = buffer.len;
+
+ const feature = self.read_register(Feature, REG_FEATURE);
+ if (feature.enable_dynamic_payload_length) {
+ const w = self.read_rx_payload_width();
+ if (w > 32) {
+ self.flush_rx();
+ return Error.NotReady;
+ }
+ width = @intCast(w);
+ if (width > buffer.len) return Error.PayloadTooLarge;
+ }
+
+ _ = self.read_command(CMD_R_RX_PAYLOAD, buffer[0..width]);
+
+ self.write_register(Status, REG_STATUS, .{
+ .rx_dr = true,
+ });
+
+ return width;
+ }
+
+ fn read_register(self: *const Self, comptime T: type, reg: u5) T {
+ const cmd: u8 = CMD_R_REGISTER | @as(u8, reg);
+ const tx_data = [_]u8{ cmd, CMD_NOP };
+ var rx_data: [2]u8 = undefined;
+
+ spi.transfer(&self.spi_config, &tx_data, &rx_data);
+
+ return @bitCast(rx_data[1]);
+ }
+
+ fn read_register_n(self: *const Self, reg: u5, buffer: []u8) void {
+ return self.read_command(CMD_R_REGISTER | @as(u8, reg), buffer);
+ }
+
+ fn read_rx_payload_width(self: *const Self) u8 {
+ const tx_data = [_]u8{ CMD_R_RX_PL_WID, CMD_NOP };
+ var rx_data: [2]u8 = undefined;
+
+ spi.transfer(&self.spi_config, &tx_data, &rx_data);
+
+ return rx_data[1];
+ }
+
+ fn write_register(self: *const Self, comptime T: type, reg: u5, value: T) void {
+ const cmd: u8 = CMD_W_REGISTER | @as(u8, reg);
+ const value_byte: u8 = @bitCast(value);
+ const tx_data = [_]u8{ cmd, value_byte };
+
+ var rx_data: [2]u8 = undefined;
+
+ spi.transfer(&self.spi_config, &tx_data, &rx_data);
+ }
+
+ fn write_register_n(self: *const Self, reg: u5, buffer: []const u8) void {
+ return self.write_command(CMD_W_REGISTER | @as(u8, reg), buffer);
+ }
+
+ fn read_command(self: *const Self, cmd: u8, result: []u8) Status {
+ var tx_data: [64]u8 = undefined;
+ var rx_data: [64]u8 = undefined;
+
+ tx_data[0] = cmd;
+ @memset(tx_data[1 .. result.len + 1], CMD_NOP);
+
+ spi.transfer(&self.spi_config, tx_data[0 .. result.len + 1], rx_data[0 .. result.len + 1]);
+
+ @memcpy(result, rx_data[1 .. result.len + 1]);
+
+ return @bitCast(rx_data[0]);
+ }
+
+ fn write_command(self: *const Self, cmd: u8, bytes: []const u8) void {
+ var tx_data: [64]u8 = undefined;
+ var rx_data: [64]u8 = undefined;
+
+ tx_data[0] = cmd;
+ @memcpy(tx_data[1 .. bytes.len + 1], bytes);
+
+ spi.transfer(&self.spi_config, tx_data[0 .. bytes.len + 1], rx_data[0 .. bytes.len + 1]);
+ }
+
+ pub fn get_status(self: *const Self) Status {
+ return self.read_register(Status, REG_STATUS);
+ }
+
+ pub fn flush_tx(self: *const Self) void {
+ const tx_data = [_]u8{CMD_FLUSH_TX};
+ var rx_data: [1]u8 = undefined;
+ spi.transfer(&self.spi_config, &tx_data, &rx_data);
+ }
+
+ pub fn flush_rx(self: *const Self) void {
+ const tx_data = [_]u8{CMD_FLUSH_RX};
+ var rx_data: [1]u8 = undefined;
+ spi.transfer(&self.spi_config, &tx_data, &rx_data);
+ }
+
+ pub fn clear_irq(self: *const Self) void {
+ self.write_register(Status, REG_STATUS, .{
+ .rx_dr = true,
+ .tx_ds = true,
+ .maxed_retries = true,
+ });
+ }
+};
diff --git a/pi/devices/spi.zig b/pi/devices/spi.zig
@@ -0,0 +1,207 @@
+const std = @import("std");
+
+const mem = @import("../mem.zig");
+const gpio = @import("./gpio.zig");
+const register = @import("../register.zig");
+
+const BASE_ADDRESS: usize = mem.BASE_ADDRESS + 0x0020_4000;
+const CS: register.Register(MasterControlStatus, .ReadModifyWrite) = .init(BASE_ADDRESS + 0x00);
+const FIFO: register.Register(u32, .ReadModifyWrite) = .init(BASE_ADDRESS + 0x04);
+const CLK: register.Register(Clk, .ReadModifyWrite) = .init(BASE_ADDRESS + 0x08);
+const DATA_LENGTH: register.Register(DataLength, .ReadModifyWrite) = .init(BASE_ADDRESS + 0x0C);
+const LTOH: register.Register(Ltoh, .ReadModifyWrite) = .init(BASE_ADDRESS + 0x10);
+const DC: register.Register(Dc, .ReadModifyWrite) = .init(BASE_ADDRESS + 0x14);
+
+const MasterControlStatus = packed struct(u32) {
+ pub const ChipSelect = enum(u2) {
+ @"0" = 0b00,
+ @"1" = 0b01,
+ @"2" = 0b10,
+ };
+
+ pub const ClockPhase = enum(u1) {
+ Middle = 0,
+ Beginning = 1,
+ };
+
+ pub const ClockPolarity = enum(u1) {
+ Low = 0,
+ High = 1,
+ };
+
+ pub const FiFoClear = packed struct(u2) {
+ clear_tx: bool,
+ clear_rx: bool,
+ };
+
+ pub const ChipSelectPolarity = enum(u1) {
+ ActiveLow = 0,
+ ActiveHigh = 1,
+ };
+
+ pub const Ren = enum(u1) {
+ Write = 0,
+ Read = 1,
+ };
+
+ pub const Len = enum(u1) {
+ SPIMaster = 0,
+ LoSSIMaster = 1,
+ };
+
+ chip_select: ChipSelect = .@"0",
+ clock_phase: ClockPhase = .Middle,
+ clock_polarity: ClockPolarity = .Low,
+ clear_fifo: FiFoClear = .{ .clear_tx = false, .clear_rx = false },
+ // elinux bcm2835 errata:
+ // "There is a CSPOL bit described here, wereas there are also CSPOL[0,1,2]
+ // described on page 153. How do these combine???"
+ // Linux sets both this and chip_select_N together
+ chip_select_polarity: ChipSelectPolarity = .ActiveLow,
+ transfer_active: bool = false,
+ dma_enable: bool = false,
+ interrupt_on_done: bool = false,
+ interrupt_on_rx: bool = false,
+ automatically_deassert_chip_select: bool = false,
+ ren: Ren = .Write,
+ len_lossi_enable: Len = .SPIMaster,
+ _unused_14: u1 = 0,
+ _unused_15: u1 = 0,
+ done: bool = false,
+ rxd_contains_data: bool = false,
+ txd_has_space: bool = true,
+ rxr_is_full: bool = false,
+ rxf_is_full: bool = false,
+ chip_select_0: ChipSelectPolarity = .ActiveLow,
+ chip_select_1: ChipSelectPolarity = .ActiveLow,
+ chip_select_2: ChipSelectPolarity = .ActiveLow,
+ dma_enable_lossi: bool = false,
+ lossi_write_u32: bool = false,
+ _reserved_31_26: u6 = 0,
+};
+
+const Clk = packed struct(u32) {
+ clock_divider: u16 = 0,
+ _reserved_31_16: u16 = 0,
+};
+
+const DataLength = packed struct(u32) {
+ length: u16,
+ _reserved_31_16: u16,
+};
+
+const Ltoh = packed struct(u32) {
+ toh: u4,
+ _reserved_31_4: u28,
+};
+
+const Dc = packed struct(u32) {
+ dma_write_request_threshold: u8,
+ dma_write_panic_threshold: u8,
+ dma_read_request_threshold: u8,
+ dma_read_panic_threshold: u8,
+};
+
+// Polled:
+// 1. Set CS(MasterControlStatus), CPOL(chip_select_polarity), CPHA(clock_phase)
+// as required and set TA(transfer_active) = 1
+// 2. Poll TXD(txd_has_space) writing bytes to SPI_FIFO(FiFo), RXD(rxd_contains_data)
+// reading bytes from SPI_FIFO(FiFo) until all data is written
+// 3. Poll DONE(transfer_progress) until it goes to 1
+// 4. Set TA(transfer_active) = 0
+pub const SpiConfig = struct {
+ chip_select: MasterControlStatus.ChipSelect,
+ chip_select_polarity: MasterControlStatus.ChipSelectPolarity = .ActiveLow,
+ clock_polarity: MasterControlStatus.ClockPolarity = .Low,
+ clock_phase: MasterControlStatus.ClockPhase = .Middle,
+ pins: enum(u8) {
+ Lower = 7,
+ Upper = 35,
+ },
+};
+
+pub fn init(config: *const SpiConfig, clock: u16) void {
+ const ce1 = @intFromEnum(config.pins);
+ gpio.fn_sel(ce1, .alt_fn_0) catch {};
+ gpio.set_pull(ce1, .Off) catch {};
+
+ gpio.fn_sel(ce1 + 1, .alt_fn_0) catch {};
+ gpio.set_pull(ce1 + 1, .Off) catch {};
+
+ gpio.fn_sel(ce1 + 2, .alt_fn_0) catch {};
+ gpio.set_pull(ce1 + 2, .Off) catch {};
+
+ gpio.fn_sel(ce1 + 3, .alt_fn_0) catch {};
+ gpio.set_pull(ce1 + 3, .Off) catch {};
+
+ gpio.fn_sel(ce1 + 4, .alt_fn_0) catch {};
+ gpio.set_pull(ce1 + 4, .Off) catch {};
+
+ // Set Clock
+ CLK.set(.{
+ .clock_divider = clock,
+ });
+}
+
+pub fn get_base_config(config: *const SpiConfig) MasterControlStatus {
+ var cs: MasterControlStatus = .{};
+
+ cs.chip_select = config.chip_select;
+ cs.chip_select_polarity = config.chip_select_polarity;
+ cs.clock_polarity = config.clock_polarity;
+ cs.clock_phase = config.clock_phase;
+
+ switch (config.chip_select) {
+ .@"0" => cs.chip_select_0 = config.chip_select_polarity,
+ .@"1" => cs.chip_select_1 = config.chip_select_polarity,
+ .@"2" => cs.chip_select_2 = config.chip_select_polarity,
+ }
+
+ return cs;
+}
+
+fn clear_fifo(config: *const SpiConfig) void {
+ var base = get_base_config(config);
+ base.clear_fifo = .{ .clear_rx = true, .clear_tx = true };
+ CS.set(base);
+}
+
+fn start_transfer(config: *const SpiConfig) void {
+ CS.set(get_base_config(config));
+ clear_fifo(config);
+ var base = get_base_config(config);
+ base.transfer_active = true;
+ CS.set(base);
+}
+
+fn end_transfer(config: *const SpiConfig) void {
+ var base = get_base_config(config);
+ base.transfer_active = false;
+ CS.set(base);
+}
+
+pub fn transfer(config: *const SpiConfig, src: []const u8, dst: []u8) void {
+ start_transfer(config);
+
+ for (0..@max(src.len, dst.len)) |i| {
+ while (!CS.get().txd_has_space) {}
+
+ if (i < src.len) {
+ FIFO.set(@as(u32, src[i]));
+ } else {
+ FIFO.set(0x00);
+ }
+
+ while (!CS.get().rxd_contains_data) {}
+
+ if (i < dst.len) {
+ dst[i] = @truncate(FIFO.get());
+ } else {
+ _ = FIFO.get();
+ }
+ }
+
+ while (!CS.get().done) {}
+
+ end_transfer(config);
+}
diff --git a/pi/register.zig b/pi/register.zig
@@ -1,6 +1,7 @@
// Referenced based off of Swift MMIO
const std = @import("std");
+const mem = @import("./mem.zig");
const Error = error{OutOfRange};
const Kind = enum {
@@ -20,11 +21,11 @@ pub fn Register(comptime T: type, comptime kind: Kind) type {
const Self = @This();
inline fn get(addr_ptr: *volatile usize) T {
- return @bitCast(addr_ptr.*);
+ return @bitCast(mem.get_u32(@volatileCast(addr_ptr)));
}
inline fn set(addr_ptr: *volatile usize, value: T) void {
- addr_ptr.* = @bitCast(value);
+ return mem.put_u32(@volatileCast(addr_ptr), @bitCast(value));
}
};
diff --git a/pi/root.zig b/pi/root.zig
@@ -24,6 +24,9 @@ pub const devices = struct {
pub const timer = @import("./devices/timer.zig");
pub const mailbox = @import("./devices/mailbox.zig");
pub const sd = @import("./devices/sd.zig");
+ pub const spi = @import("./devices/spi.zig");
+ pub const mini_spi = @import("./devices/mini-spi.zig");
+ pub const nrf = @import("./devices/nrf.zig");
};
pub const fs = struct {
diff --git a/programs/nrf-client.zig b/programs/nrf-client.zig
@@ -0,0 +1,66 @@
+const std = @import("std");
+const shared = @import("shared");
+const pi = @import("pi");
+
+const nrf = pi.devices.nrf;
+const clock = pi.devices.clock;
+const uart = pi.devices.mini_uart;
+
+fn wait_for_ans(client_dev: *nrf.Device) !u8 {
+ var buffer: [4]u8 = undefined;
+
+ while (true) {
+ const bytes_collected = client_dev.receive(&buffer) catch |err| {
+ if (err == nrf.Error.NotReady) continue;
+ return err;
+ };
+ if (bytes_collected == 4) break;
+ if (bytes_collected > 0) {
+ uart.print("read: {d}\n", .{bytes_collected});
+ }
+
+ clock.delay_ms(1);
+ }
+
+ return buffer[3];
+}
+
+// Server:
+// CS - 8
+// CE - 6
+//
+// Client:
+// CS - 7
+// CE - 5
+pub fn main() !void {
+ uart.print("creating client\n", .{});
+
+ const client_addr = 0xE5E5E5E5E5;
+
+ var client_dev: nrf.Device = try .init(.{
+ .spi_config = .{
+ .chip_select = .@"1",
+ .pins = .Lower,
+ },
+ .ce_pin = 5,
+ .int_pin = 22,
+ .payload_size = 4,
+ });
+ uart.print("initialized client\n", .{});
+
+ try client_dev.set_rx_addr(0, client_addr);
+ client_dev.connect();
+
+ while (true) {
+ const b = try wait_for_ans(&client_dev);
+
+ uart.print("got byte: {c}\n", .{b});
+
+ clock.delay_ms(10);
+
+ if (b == '\r') break;
+ }
+
+ uart.print("done!\n", .{});
+ uart.flush();
+}
diff --git a/programs/nrf-server.zig b/programs/nrf-server.zig
@@ -0,0 +1,53 @@
+const std = @import("std");
+const shared = @import("shared");
+const pi = @import("pi");
+
+const nrf = pi.devices.nrf;
+const clock = pi.devices.clock;
+const uart = pi.devices.mini_uart;
+
+// Server:
+// CS - 8
+// CE - 6
+//
+// Client:
+// CS - 7
+// CE - 5
+pub fn main() !void {
+ uart.print("creating server\n", .{});
+
+ const client_addr = 0xE5E5E5E5E5;
+
+ var server_dev: nrf.Device = try .init(.{
+ .spi_config = .{
+ .chip_select = .@"0",
+ .pins = .Lower,
+ },
+ .ce_pin = 6,
+ .int_pin = 23,
+ .payload_size = 4,
+ });
+ uart.print("initialized server\n", .{});
+
+ server_dev.set_tx_addr(client_addr);
+ try server_dev.set_rx_addr(0, client_addr);
+
+ const msg = "Hello, World!\r";
+
+ for (msg) |b| {
+ uart.print("Sending {c}: ", .{b});
+
+ var buffer: [4]u8 = .{ 0, 0, 0, b };
+ if (try server_dev.transmit(&buffer, 1000)) {
+ uart.print("transmitted\n", .{});
+ } else {
+ uart.print("server failed to transmit\n", .{});
+ continue;
+ }
+
+ clock.delay_ms(10);
+ }
+
+ uart.print("done!\n", .{});
+ uart.flush();
+}
diff --git a/programs/nrf.zig b/programs/nrf.zig
@@ -0,0 +1,78 @@
+const std = @import("std");
+const shared = @import("shared");
+const pi = @import("pi");
+
+const nrf = pi.devices.nrf;
+const uart = pi.devices.mini_uart;
+
+// Server:
+// CS - 8
+// CE - 6
+//
+// Client:
+// CS - 7
+// CE - 5
+pub fn main() !void {
+ uart.print("creating client/server\n", .{});
+
+ const client_addr = 0xE5E5E5E5E5;
+ // const server_addr = 0xD5D5D5;
+
+ var client_dev: nrf.Device = try .init(.{
+ .spi_config = .{
+ .chip_select = .@"1",
+ .pins = .Lower,
+ },
+ .ce_pin = 5,
+ .int_pin = 22,
+ .payload_size = 4,
+ });
+ uart.print("initialized client\n", .{});
+
+ var server_dev: nrf.Device = try .init(.{
+ .spi_config = .{
+ .chip_select = .@"0",
+ .pins = .Lower,
+ },
+ .ce_pin = 6,
+ .int_pin = 23,
+ .payload_size = 4,
+ });
+ uart.print("initialized server\n", .{});
+
+ uart.print("running tests\n", .{});
+ server_dev.set_tx_addr(client_addr);
+ try server_dev.set_rx_addr(0, client_addr);
+ try client_dev.set_rx_addr(0, client_addr);
+ client_dev.connect();
+
+ for (0..1000) |i| {
+ uart.print("trial {}: ", .{i});
+
+ {
+ var buffer: [4]u8 = undefined;
+ std.mem.writeInt(u32, &buffer, i, .little);
+ if (!try server_dev.transmit(&buffer, 128)) {
+ uart.print("server failed to transmit\n", .{});
+ continue;
+ }
+ }
+
+ {
+ var buffer: [4]u8 = undefined;
+ if (try client_dev.receive(&buffer) > 0) {
+ const x = std.mem.readInt(u32, &buffer, .little);
+
+ if (x == i) {
+ uart.print("{} == {}\n", .{ x, i });
+ } else {
+ uart.print("{} != {}\n", .{ x, i });
+ }
+ } else {
+ uart.print("timed out\n", .{});
+ }
+ }
+ }
+
+ uart.print("done!\n", .{});
+}