diff --git a/drivers/build.zig.zon b/drivers/build.zig.zon index 206e63a44..33813fc0f 100644 --- a/drivers/build.zig.zon +++ b/drivers/build.zig.zon @@ -1,6 +1,7 @@ .{ .name = .mz_drivers, .fingerprint = 0x576453eedc5af46e, + .minimum_zig_version = "0.15.1", .version = "0.0.1", .paths = .{ "build.zig", diff --git a/drivers/framework.zig b/drivers/framework.zig index fc49a94b9..dc6a43414 100644 --- a/drivers/framework.zig +++ b/drivers/framework.zig @@ -77,6 +77,8 @@ pub const wireless = struct { pub const Cyw43_Bus = cyw43_bus.Cyw43_Bus; pub const Cyw43_Runner = cyw43_runner.Cyw43_Runner; // pub const sx1278 = @import("wireless/sx1278.zig"); + + pub const Cyw43439 = @import("wireless/cyw43439.zig"); }; pub const time = struct { diff --git a/drivers/wireless/cyw43439.zig b/drivers/wireless/cyw43439.zig new file mode 100644 index 000000000..350efea75 --- /dev/null +++ b/drivers/wireless/cyw43439.zig @@ -0,0 +1,99 @@ +const std = @import("std"); +const mem = std.mem; +const assert = std.debug.assert; + +const Bus = @import("cyw43439/bus.zig"); +const WiFi = @import("cyw43439/wifi.zig"); + +const log = std.log.scoped(.cyw43); + +const Self = @This(); + +bus: Bus = undefined, +wifi: WiFi = undefined, +mac: [6]u8 = @splat(0), + +pub fn init( + self: *Self, + spi: Bus.Spi, + sleep_ms: *const fn (delay: u32) void, +) !void { + self.bus = .{ .spi = spi, .sleep_ms = sleep_ms }; + try self.bus.init(); + + self.wifi = .{ .bus = &self.bus }; + try self.wifi.init(); + + self.mac = try self.read_mac(); +} + +pub fn join(self: *Self, ssid: []const u8, pwd: []const u8, opt: WiFi.JoinOptions) !void { + try self.wifi.join(ssid, pwd, opt); +} + +fn show_clm_ver(self: *Self) !void { + var data: [128]u8 = @splat(0); + const n = try self.wifi.get_var("clmver", &data); + var iter = mem.splitScalar(u8, data[0..n], 0x0a); + log.debug("clmver:", .{}); + while (iter.next()) |line| { + if (line.len == 0 or line[0] == 0x00) continue; + log.debug(" {s}", .{line}); + } +} + +fn read_mac(self: *Self) ![6]u8 { + var mac: [6]u8 = @splat(0); + const n = try self.wifi.get_var("cur_etheraddr", &mac); + if (n != mac.len) { + log.err("read_mac unexpected read bytes: {}", .{n}); + return error.ReadMacFailed; + } + return mac; +} + +pub fn recv_zc(ptr: *anyopaque, bytes: []u8) anyerror!?struct { usize, usize } { + const self: *Self = @ptrCast(@alignCast(ptr)); + return self.wifi.recv_zc(bytes); +} + +pub fn send_zc(ptr: *anyopaque, bytes: []u8) anyerror!void { + const self: *Self = @ptrCast(@alignCast(ptr)); + try self.wifi.send_zc(bytes); +} + +pub fn ready(ptr: *anyopaque) bool { + const self: *Self = @ptrCast(@alignCast(ptr)); + return self.wifi.has_credit(); +} + +pub fn gpio(self: *Self, pin: u2) Pin { + assert(pin < 3); + self.wifi.gpio_enable(pin); + return .{ + .pin = pin, + .wifi = &self.wifi, + }; +} + +pub const Pin = struct { + pin: u2, + wifi: *WiFi, + + pub fn get(self: *Pin) bool { + return self.wifi.gpio_get(self.pin); + } + + pub fn put(self: *Pin, value: u1) void { + self.wifi.gpio_set(self.pin, value); + } + + pub fn toggle(self: *Pin) void { + self.wifi.gpio_toggle(self.pin); + } +}; + +// References: +// https://github.com/embassy-rs/embassy/blob/abb1d8286e2415686150e2e315ca1c380659c3c3/cyw43/src/consts.rs +// https://github.com/jbentham/picowi/blob/main/lib/picowi_regs.h +// https://github.com/georgerobotics/cyw43-driver/blob/dd7568229f3bf7a37737b9e1ef250c26efe75b23/src/cyw43_ll.c#L126 diff --git a/drivers/wireless/cyw43439/bus.zig b/drivers/wireless/cyw43439/bus.zig new file mode 100644 index 000000000..899b42ada --- /dev/null +++ b/drivers/wireless/cyw43439/bus.zig @@ -0,0 +1,396 @@ +/// CYW43 bus controller (SPI + power management) +/// +const std = @import("std"); +const assert = std.debug.assert; + +const log = std.log.scoped(.cyw43_bus); + +pub const Spi = struct { + ptr: *anyopaque, + vtable: *const VTable, + + /// Reads data from SPI into buffer, first word in buffer is command + pub fn read(self: *@This(), buffer: []u32) void { + return self.vtable.read(self.ptr, buffer); + } + + /// Writes buffer to SPI, first word in buffer is command + pub fn write(self: *@This(), buffer: []u32) void { + self.vtable.write(self.ptr, buffer); + } + + pub const VTable = struct { + read: *const fn (*anyopaque, []u32) void, + write: *const fn (*anyopaque, []u32) void, + }; +}; + +const Self = @This(); + +spi: Spi, +sleep_ms: *const fn (delay: u32) void, +backplane_window: u32 = 0xAAAA_AAAA, + +pub fn init(self: *Self) !void { + // First read of read only test register + out: { + var val: u32 = 0; + for (0..8) |_| { + val = self.read_swapped(.bus, reg.test_read_only); + if (val == reg.test_read_only_content) + break :out; + self.sleep_ms(2); + } + log.err("ro register first read unexpected value: {x}", .{val}); + return error.Cyw43RoTestReadFailed; + } + + const test_rw_value: u32 = 0x12345678; + // First write/read of read/write test register + { + self.write_swapped(.bus, reg.test_read_write, test_rw_value); + const val = self.read_swapped(.bus, reg.test_read_write); + if (val != test_rw_value) { + log.err("rw register first read unexpected value: {x}", .{val}); + return error.Cyw43RwTestReadFailed; + } + } + // Write setup registers + { + _ = self.read_swapped(.bus, reg.control); + // Set 32-bit word length and keep default endianness: little endian + const setup_regs = SetupRegs{ + .control = .{ + .word_length = .word_32, + .endianness = .little, + .speed_mode = .high, + .interrupt_polarity = .high, + .wake_up = true, + }, + .response_delay = .{ + .unknown = 0x4, // 32-bit response delay? + }, + .status_enable = .{ + .status_enable = true, + .interrupt_with_status = true, + }, + }; + self.write_swapped(.bus, reg.control, @bitCast(setup_regs)); + } + // Second read of ro and rw registers + { + const ro_val = self.read_int(u32, .bus, reg.test_read_only); + if (ro_val != reg.test_read_only_content) { + log.err("ro register second read unexpected value: {x}", .{ro_val}); + return error.Cyw43SecondTestReadFailed; + } + + const rw_val = self.read_int(u32, .bus, reg.test_read_write); + if (rw_val != test_rw_value) { + log.err("rw register second read unexpected value: {x}", .{rw_val}); + return error.Cyw43SecondTestReadFailed; + } + } + // Set interrupts + { + // Clear error interrupt bits + self.write_int(u16, .bus, reg.interrupt, @bitCast(Irq.clear)); + // Enable a selection of interrupts + self.write_int(u16, .bus, reg.interrupt_enable, @bitCast(Irq{ + .f2_f3_fifo_rd_underflow = true, + .f2_f3_fifo_wr_overflow = true, + .command_error = true, + .data_error = true, + .f2_packet_available = true, + .f1_overflow = true, + })); + } + { // bluetooth... + self.write_int(u8, .bus, bt.spi_resp_delay_f1, bt.whd_bus_spi_backplane_read_padd_size); + } +} + +pub fn read_int(self: *Self, T: type, func: Cmd.Func, waddr: u32) T { + const addr: u17 = if (func == .backplane and waddr > backplane.sleep_csr) + self.backplane_window_addr(waddr, @sizeOf(T)) + else + @intCast(waddr); + + var buf: [3]u32 = @splat(0); + // When making a ‘backplane’ read, the first 4 return bytes are + // discarded; they are padding to give the remote peripheral time to + // respond. Last word is status. + const buf_len: usize = if (func == .backplane) 3 else 2; + self.read(func, addr, @sizeOf(T), buf[0..buf_len]); + return @truncate(if (func == .backplane) buf[1] else buf[0]); +} + +// len - number of bytes to read +// buffer must have one word for status at the end +pub fn read(self: *Self, func: Cmd.Func, addr: u17, len: u11, buf: []u32) void { + assert(@divFloor(len + 3, 4) < buf.len); + const cmd = Cmd{ .kind = .read, .func = func, .addr = addr, .len = len }; + buf[0] = @bitCast(cmd); + self.spi.read(buf); +} + +pub fn write_int(self: *Self, T: type, func: Cmd.Func, waddr: u32, value: T) void { + const addr: u17 = if (func == .backplane and waddr > backplane.sleep_csr) + self.backplane_window_addr(waddr, @sizeOf(T)) + else + @intCast(waddr); + var buf: [2]u32 = @splat(0); + buf[1] = @intCast(value); + self.write(func, addr, @sizeOf(T), &buf); +} + +pub fn write(self: *Self, func: Cmd.Func, addr: u17, len: u11, buf: []u32) void { + const cmd = Cmd{ .kind = .write, .func = func, .addr = addr, .len = len }; + buf[0] = @bitCast(cmd); + self.spi.write(buf); + // status is in the first word of buf +} + +pub fn backplane_read(self: *Self, addr: u32, data: []u8) void { + var words: [backplane.max_transfer_size / 4 + 2]u32 = undefined; + var current_addr = addr; + var remaining_data = data; + + while (remaining_data.len > 0) { + const window_offs = current_addr & backplane.address_mask; + const window_remaining = backplane.window_size - @as(usize, @intCast(window_offs)); + const len: usize = @min(remaining_data.len, backplane.max_transfer_size, window_remaining); + + self.backplane_set_window(current_addr); + + const cmd = Cmd{ + .kind = .read, + .func = .backplane, + .addr = @truncate(window_offs), + .len = @truncate(len), + }; + // round `buf` to word boundary + // add one extra word for the response delay (at the start) + // and one word for status (at the end) + const wlen = 1 + ((len + 3) >> 2) + 1; + words[0] = @bitCast(cmd); + _ = self.spi.read(words[0..wlen]); + + var buf = std.mem.sliceAsBytes(words[1 .. wlen - 1]); + @memcpy(remaining_data[0..len], buf[0..len]); + + current_addr += @as(u32, @intCast(len)); + remaining_data = remaining_data[len..]; + } +} + +pub fn backplane_write(self: *Self, addr: u32, data: []const u8) void { + // write buffer in words, 1 word at the start for the bus cmd + var words: [backplane.max_transfer_size / 4 + 1]u32 = undefined; + + var current_addr = addr; + var remaining = data; + while (remaining.len > 0) { + const window_offset = current_addr & backplane.address_mask; + const window_remaining = backplane.window_size - @as(usize, @intCast(window_offset)); + const len: usize = @min(remaining.len, backplane.max_transfer_size, window_remaining); + const wlen = (len + 3) >> 2; // len in words + + // copy to words buffer + @memcpy(std.mem.sliceAsBytes(words[1..])[0..len], remaining[0..len]); + // write + self.backplane_set_window(current_addr); + self.write(.backplane, @truncate(window_offset), @intCast(len), words[0 .. wlen + 1]); + + current_addr += @as(u32, @intCast(len)); + remaining = remaining[len..]; + } +} + +fn backplane_window_addr(self: *Self, addr: u32, len: u11) u17 { + self.backplane_set_window(addr); + var bus_addr = addr & backplane.address_mask; + if (len == 4) { + bus_addr |= backplane.address_32bit_flag; + } + return @truncate(bus_addr); +} + +fn backplane_set_window(self: *Self, addr: u32) void { + const new_window = addr & ~backplane.address_mask; + + if (@as(u8, @truncate(new_window >> 24)) != @as(u8, @truncate(self.backplane_window >> 24))) { + self.write_int(u8, .backplane, backplane.address_high, @as(u8, @truncate(new_window >> 24))); + } + if (@as(u8, @truncate(new_window >> 16)) != @as(u8, @truncate(self.backplane_window >> 16))) { + self.write_int(u8, .backplane, backplane.address_mid, @as(u8, @truncate(new_window >> 16))); + } + if (@as(u8, @truncate(new_window >> 8)) != @as(u8, @truncate(self.backplane_window >> 8))) { + self.write_int(u8, .backplane, backplane.address_low, @as(u8, @truncate(new_window >> 8))); + } + + self.backplane_window = new_window; +} + +fn read_swapped(self: *Self, func: Cmd.Func, addr: u17) u32 { + const cmd = Cmd{ .kind = .read, .func = func, .addr = addr, .len = 4 }; + var buf: [2]u32 = @splat(0); // second word is status + buf[0] = swap16(@bitCast(cmd)); + self.spi.read(&buf); + return swap16(buf[0]); +} + +fn write_swapped(self: *Self, func: Cmd.Func, addr: u17, value: u32) void { + const cmd = Cmd{ .kind = .write, .func = func, .addr = addr, .len = 4 }; + var buf: [2]u32 = @splat(0); + buf[0] = swap16(@bitCast(cmd)); + buf[1] = swap16(value); + self.spi.write(&buf); +} + +inline fn swap16(x: u32) u32 { + return x << 16 | x >> 16; +} + +// ref: datasheet page 19 +const Cmd = packed struct(u32) { + const Kind = enum(u1) { + read = 0, + write = 1, + }; + + const Access = enum(u1) { + fixed = 0, // Fixed address (no increment) + incremental = 1, // Incremental burst + }; + + const Func = enum(u2) { + bus = 0, // Func 0: All SPI-specific registers + backplane = 1, // Func 1: Registers and memories belonging to other blocks in the chip (64 bytes max) + wlan = 2, // Func 2: DMA channel 1. WLAN packets up to 2048 bytes. + bt = 3, // Func 3: DMA channel 2 (optional). Packets up to 2048 bytes. + }; + + len: u11, // Packet length, max 2048 bytes + addr: u17, + func: Func = .bus, + access: Access = .incremental, + kind: Kind, +}; + +const SetupRegs = packed struct(u32) { + const Control = packed struct(u8) { + const WordLength = enum(u1) { + word_16 = 0, + word_32 = 1, + }; + + const Endianness = enum(u1) { + little = 0, + big = 1, + }; + + const SpeedMode = enum(u1) { + normal = 0, + high = 1, + }; + + const InterruptPolarity = enum(u1) { + low = 0, + high = 1, + }; + + word_length: WordLength, + endianness: Endianness, + _reserved1: u2 = 0, + speed_mode: SpeedMode, + interrupt_polarity: InterruptPolarity, + _reserved2: u1 = 0, + wake_up: bool, + }; + const ResponseDelay = packed struct(u8) { + unknown: u8, + }; + const StatusEnable = packed struct(u8) { + status_enable: bool, + interrupt_with_status: bool, + _reserved1: u6 = 0, + }; + + control: Control, + response_delay: ResponseDelay, + status_enable: StatusEnable, + _reserved1: u8 = 0, +}; + +// Register addresses +// reference datasheet page 22 'Table 6. gSPI Registers' +pub const reg = struct { + const control: u32 = 0x0; + const response_delay: u32 = 0x1; + const status_enable: u32 = 0x2; + + pub const interrupt: u32 = 0x04; // 16 bits - Interrupt status + pub const interrupt_enable: u32 = 0x06; // 16 bits - Interrupt mask + pub const status: u32 = 0x8; + + // This register contains a predefined pattern, which the host can read to + // determine if the gSPI interface is working properly. + const test_read_only: u32 = 0x14; + const test_read_only_content: u32 = 0xFEEDBEAD; + // This is a dummy register where the host can write some pattern and read it + // back to determine if the gSPI interface is working properly. + const test_read_write: u32 = 0x18; + //pub const RESP_DELAY: u32 = 0x1c; +}; + +// interrupt register and interrupt enable register bits +pub const Irq = packed struct { + data_unavailable: bool = false, // Requested data not available; Clear by writing a "1" + f2_f3_fifo_rd_underflow: bool = false, + f2_f3_fifo_wr_overflow: bool = false, + command_error: bool = false, // Cleared by writing 1 + data_error: bool = false, // Cleared by writing 1 + f2_packet_available: bool = false, + f3_packet_available: bool = false, + f1_overflow: bool = false, // Due to last write. Bkplane has pending write requests + misc_intr0: bool = false, + misc_intr1: bool = false, + misc_intr2: bool = false, + misc_intr3: bool = false, + misc_intr4: bool = false, + f1_intr: bool = false, + f2_intr: bool = false, + f3_intr: bool = false, + + const clear = Irq{ + .data_unavailable = true, + .command_error = true, + .data_error = true, + .f1_overflow = true, + }; +}; + +pub const backplane = struct { + const window_size: usize = 0x8000; + const address_mask: u32 = 0x7FFF; + const address_32bit_flag: u32 = 0x08000; + const max_transfer_size: usize = 64; + + pub const function2_watermark: u32 = 0x10008; + pub const address_low: u32 = 0x1000A; + pub const address_mid: u32 = 0x1000B; + pub const address_high: u32 = 0x1000C; + pub const chip_clock_csr: u32 = 0x1000E; + pub const pull_up: u32 = 0x1000F; + pub const sleep_csr: u32 = 0x1001F; + + pub const alp_avail_req: u8 = 0x08; + pub const alp_avail: u8 = 0x40; +}; + +// Bluetooth constants. +const bt = struct { + const spi_resp_delay_f1: u32 = 0x001d; + const whd_bus_spi_backplane_read_padd_size: u8 = 4; +}; diff --git a/drivers/wireless/cyw43439/ioctl.zig b/drivers/wireless/cyw43439/ioctl.zig new file mode 100644 index 000000000..3cc867a22 --- /dev/null +++ b/drivers/wireless/cyw43439/ioctl.zig @@ -0,0 +1,719 @@ +//! An IOCTL (Input/Output Control) call is sent by the Pico host CPU (RP2040) +//! to the ARM CPU in the WiFi chip, to read or write configuration data, or send +//! a specific command. +//! +//! An event is an unsolicited block of data sent from the WiFi CPU to the host; +//! it can be a notification that an action is complete, or some data that has +//! arrived over the WiFi network. +//! +//! https://iosoft.blog/2022/12/06/picowi_part3/ +//! +//! https://github.com/embassy-rs/embassy/blob/main/cyw43/src/structs.rs +//! https://github.com/jbentham/picowi/blob/main/lib/picowi_ioctl.h +//! +const std = @import("std"); +const mem = std.mem; +const assert = std.debug.assert; +const testing = std.testing; + +const log = std.log.scoped(.cyw43_ioctl); + +// Time to wait for ioctl response (msec) +pub const response_wait = response_poll_interval * 8; +// Polling interval for ioctl responses +pub const response_poll_interval = 10; + +// SDPCM (Serial Data Packet Communication Module) header +// SDIO/SPI Bus Layer (SDPCM) header +// ref: https://iosoft.blog/2022/12/06/picowi_part3/ +const SdpHeader = extern struct { + len: u16, + not_len: u16, + /// Rx/Tx sequence number + seq: u8, + /// 4 MSB Channel, 4 LSB arbitrary flags + cf: ChanFlags, + /// Length of next data frame, reserved for Tx + nextlen: u8 = 0, + /// Data offset from the start of the packet. + /// Includes BusHeader length and padding after BusHeader. + hdrlen: u8, + /// Flow control bits, reserved for Tx + flow: u8 = 0, + /// Maximum Sequence number allowed by firmware for Tx + credit: u8 = 0, + _reserved: u16 = 0, + + const Chan = enum(u4) { + control = 0, + event = 1, + data = 2, + }; + + const ChanFlags = packed struct(u8) { + chan: Chan, + flags: u4 = 0, + }; + + pub fn channel(sdp: SdpHeader) Chan { + return sdp.cf.chan; + } + + pub fn validate(sdp: SdpHeader, n: u16) !void { + if (sdp.len != n) { + log.err("invalid reponse len actual: {} packet: {}", .{ sdp.len, n }); + return error.Cyw43SdpInvalidBusLen; + } + if (sdp.len ^ sdp.not_len != 0xffff) { + log.err("invalid reponse not len len: {x} notlen: {x}", .{ sdp.len, sdp.not_len }); + return error.Cyw43SdpInvalidNotBusLen; + } + } +}; + +const CdcHeader = extern struct { + cmd: Cmd, + outlen: u16, + inlen: u16 = 0, + flags: u16 = 0, + id: u16, + status: u32 = 0, + + pub fn status_ok(self: CdcHeader) bool { + //dev/ apsta, ampdu_rx_factor commands are returing status 0xFFFFFFFB + return self.status == 0 or self.status == 0xFFFFFFFB; + } +}; + +pub const Cmd = enum(u32) { + up = 2, + down = 3, + set_infra = 20, + set_auth = 22, + set_ssid = 26, + set_antdiv = 64, + set_gmode = 110, + set_wsec = 134, + set_band = 142, + set_wpa_auth = 165, + get_var = 262, + set_var = 263, + set_wsec_pmk = 268, + _, +}; + +const BdcHeader = extern struct { + flags: u8, + priority: u8, + flags2: u8, + offset: u8, + + // Padding after bdc header where data or event bytes starts + fn padding(self: BdcHeader) usize { + return @as(usize, self.offset) * 4; + } +}; + +// Structure: +// - 12 bytes sdp header +// - xx padding bytes (defined by BusHeader.hdrlen) +// than if chan == .control +// - 16 bytes of cdc header +// - xx result data in the case of get_var control command +// if chan == .event +// - 4 bytes bdc header +// - xx bdc padding bytes (defined in BdcHeader.offset) +// - 73 bytes of EventPacket +// - other event bytes +// if chan == .data +// - 4 bytes bdc header +// - bdc padding = bdc.offset * 4 +// - data, from this position to the end of the packet +// +// For data packet response, sdp padding is 2 bytes, ad bdc padding 4 bytes, +// that gives 12 + 2 + 4 + 4 = 22 bytes of bus header, before data. +// +pub const Response = struct { + const Self = @This(); + + sdp: SdpHeader, + buffer: []const u8, + + pub fn init(buf: []const u8) !Self { + const sdp: SdpHeader = @bitCast(buf[0..@sizeOf(SdpHeader)].*); + try sdp.validate(@intCast(buf.len)); + return .{ + .sdp = sdp, + .buffer = buf[@sizeOf(SdpHeader)..], + }; + } + + // Number of padding bytes after bus header before cdc/bdc header + fn padding(self: Self) usize { + return self.sdp.hdrlen - @sizeOf(SdpHeader); + } + + pub fn cdc(self: Self) CdcHeader { + assert(self.sdp.channel() == .control); + return @bitCast(self.buffer[self.padding()..][0..@sizeOf(CdcHeader)].*); + } + + pub fn bdc(self: Self) BdcHeader { + assert(self.sdp.channel() != .control); + return @bitCast(self.buffer[self.padding()..][0..@sizeOf(BdcHeader)].*); + } + + pub fn data(self: Self) []const u8 { + const head: usize = self.padding() + switch (self.sdp.channel()) { + .control => @sizeOf(CdcHeader), + .event, .data => @sizeOf(BdcHeader) + self.bdc().padding(), + }; + const tail = self.sdp.len - @sizeOf(SdpHeader); + if (head > tail) { + return &.{}; + } + return self.buffer[head..tail]; + } + + // head position and length of the data in the buffer sent to init + pub fn data_pos(self: Self) struct { usize, usize } { + const head: usize = self.sdp.hdrlen + switch (self.sdp.channel()) { + .control => @sizeOf(CdcHeader), + .event, .data => @sizeOf(BdcHeader) + self.bdc().padding(), + }; + return .{ head, self.sdp.len - head }; + } + + pub fn event(self: Self) EventPacket { + assert(self.sdp.channel() == .event); + const buf = self.data(); + if (buf.len < @sizeOf(EventPacket)) { + var zero = mem.zeroes(EventPacket); + zero.msg.event_type = .none; + return zero; + } + var evt: EventPacket = @bitCast(buf[0..@sizeOf(EventPacket)].*); + std.mem.byteSwapAllFields(EventPacket, &evt); + return evt; + } +}; + +pub fn response(buf: []const u8) !Response { + return Response.init(buf); +} + +/// Format command request +pub fn request( + buf: []u8, + cmd: Cmd, + name: []const u8, + data: []const u8, + request_id: u16, + tx_sequence: u8, +) []const u8 { + // name length with sentinel + const name_len: usize = name.len + if (name.len > 0) @as(usize, 1) else @as(usize, 0); + // length of name and data rounded to 4 bytes + const payload_len: u16 = @intCast(((name_len + (if (data.len > 0 and data.len < 4) 4 else data.len) + 3) >> 2) * 4); + const header_len: u16 = @sizeOf(SdpHeader) + @sizeOf(CdcHeader); + const txlen: u16 = header_len + payload_len; + + const sdp: SdpHeader = .{ + .len = txlen, + .not_len = ~txlen, + .seq = tx_sequence, + .cf = .{ + .chan = .control, + .flags = 0, + }, + .hdrlen = @sizeOf(SdpHeader), + }; + buf[0..@sizeOf(SdpHeader)].* = @bitCast(sdp); + const cdc: CdcHeader = .{ + .cmd = cmd, + .outlen = payload_len, + .id = request_id, + .flags = if (data.len > 0) 0x02 else 0, + }; + buf[@sizeOf(SdpHeader)..][0..@sizeOf(CdcHeader)].* = @bitCast(cdc); + if (name_len > 0) { + @memcpy(buf[header_len..][0..name.len], name); + buf[header_len..][name.len] = 0; // sentinel + } + if (data.len > 0) { + @memcpy(buf[header_len + name_len ..][0..data.len], data); + } + // set paddnig bytes to 0 + for (header_len + name_len + data.len..txlen) |i| { + buf[i] = 0; + } + return buf[0..txlen]; +} + +pub const TxHeader = extern struct { + const Self = @This(); + + sdp: SdpHeader = mem.zeroes(SdpHeader), + _padding: u16 = 0, + bdc: BdcHeader = mem.zeroes(BdcHeader), + + pub fn init(data_len: u16, tx_sequence: u8) Self { + const txlen = @sizeOf(TxHeader) + data_len; + var self: Self = .{}; + self.sdp = .{ + .len = txlen, + .not_len = ~txlen, + .seq = tx_sequence, + .cf = .{ + .chan = .data, + .flags = 0, + }, + .hdrlen = @sizeOf(SdpHeader) + 2, + }; + self.bdc.flags = 0x20; + return self; + } +}; + +pub const tx_header_len = @sizeOf(TxHeader); + +pub fn tx_header(data_len: u16, tx_sequence: u8) [tx_header_len]u8 { + return @bitCast(TxHeader.init(data_len, tx_sequence)); +} + +comptime { + assert(@sizeOf(SdpHeader) == 12); + assert(@sizeOf(BdcHeader) == 4); + assert(@sizeOf(CdcHeader) == 16); + assert(@sizeOf(EventPacket) == 72); + assert(@sizeOf(TxHeader) == 18); +} + +test "request" { + { + var buf: [64]u8 = undefined; + const req = request(&buf, .get_var, "cur_etheraddr", &.{}, 3, 3); + + const expected = &hex_to_bytes("2C00D3FF0300000C00000000060100001000000000000300000000006375725F657468657261646472000000"); + try std.testing.expectEqualSlices(u8, expected, req); + } + { + const expected = &hex_to_bytes("2C00D3FF0700000C00000000070100001000000002000700000000006770696F6F7574000100000001000000"); + var data: [8]u8 = @splat(0); + data[0] = 1; + data[4] = 1; + var buf: [64]u8 = undefined; + const req = request(&buf, .set_var, "gpioout", &data, 7, 7); + try std.testing.expectEqualSlices(u8, expected, req); + } +} + +test "parse response" { + const expected = &hex_to_bytes("2CCF67F3B7EA"); + const rsp_bytes = &hex_to_bytes("0001FFFE040000DC0014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060100001400000000000300000000002CCF67F3B7EA6865726164647200000000000000"); + try testing.expectEqual(256, rsp_bytes.len); + const rsp = try Response.init(rsp_bytes); + + try testing.expectEqual(256, rsp.sdp.len); + try testing.expectEqual(0xffff, rsp.sdp.len ^ rsp.sdp.not_len); + try testing.expectEqual(4, rsp.sdp.seq); + try testing.expectEqual(220, rsp.sdp.hdrlen); + try testing.expectEqual(20, rsp.sdp.credit); + try testing.expectEqual(0, rsp.sdp.nextlen); + + const ioctl = rsp.cdc(); + try testing.expectEqual(.get_var, ioctl.cmd); + try testing.expectEqual(20, ioctl.outlen); + try testing.expectEqual(0, ioctl.inlen); + try testing.expectEqual(3, ioctl.id); + try testing.expectEqual(0, ioctl.flags); + try testing.expectEqual(0, ioctl.status); + + try testing.expectEqualSlices(u8, expected, rsp.data()[0..expected.len]); + const head, const len = rsp.data_pos(); + try testing.expectEqual(236, head); + try testing.expectEqual(20, len); + try testing.expectEqualSlices(u8, expected, rsp_bytes[head..][0..len][0..expected.len]); +} + +pub fn hex_to_bytes(comptime hex: []const u8) [hex.len / 2]u8 { + var res: [hex.len / 2]u8 = undefined; + _ = std.fmt.hexToBytes(&res, hex) catch unreachable; + return res; +} + +const EventPacket = extern struct { + eth: EthernetHeader, + hdr: EventHeader, + msg: EventMessage, +}; + +// Escan result event (excluding 12-byte IOCTL header and BDC header) +const EventScanResult = extern struct { + eth: EthernetHeader, + hdr: EventHeader, + msg: EventMessage, + scan: ScanResultHeader, + info: BssInfo, +}; + +// Ethernet header (sdpcm_ethernet_header_t) +const EthernetHeader = extern struct { + dest_mac: [6]u8, + source_mac: [6]u8, + ether_type: u16, +}; + +// Vendor-specific (Broadcom) Ethernet header (sdpcm_bcmeth_header_t) +const EventHeader = extern struct { + subtype: u16, + len: u16, + ver: u8, + oui: [3]u8, + usr_subtype: u16, +}; + +// Raw event header (sdpcm_raw_event_header_t) +const EventMessage = extern struct { + version: u16, + flags: u16, + event_type: EventType, + status: EventStatus, + reason: u32, + auth_type: u32, + datalen: u32, + addr: [6]u8, + ifname: [16]u8, + ifidx: u8, + bsscfgidx: u8, +}; + +// Scan result header (part of wl_escan_result_t) +const ScanResultHeader = extern struct { + buflen: u32, + version: u32, + sync_id: u16, + bss_count: u16, +}; + +// BSS info from EScan (part of wl_bss_info_t) +const BssInfo = extern struct { + version: u32, // version field + length: u32, // byte length of data in this record, starting at version and including IEs + bssid: [6]u8, // Unique 6-byte MAC address + beacon_period: u16, // Interval between two consecutive beacon frames. Units are Kusec + capability: u16, // Capability information + ssid_len: u8, // SSID length + ssid: [32]u8, // Array to store SSID + nrates: u32, // Count of rates in this set + rates: [16]u8, // rates in 500kbps units, higher bit set if basic + channel: u16, // Channel specification for basic service set + atim_window: u16, // Announcement traffic indication message window size. Units are Kusec + dtim_period: u8, // Delivery traffic indication message period + rssi: u16, // receive signal strength (in dBm) + phy_noise: u8, // noise (in dBm) + // The following fields assume the 'version' field is 109 (0x6D) + n_cap: u8, // BSS is 802.11N Capable + nbss_cap: u32, // 802.11N BSS Capabilities (based on HT_CAP_*) + ctl_ch: u8, // 802.11N BSS control channel number + reserved1: u32, // Reserved for expansion of BSS properties + flags: u8, // flags + reserved2: [3]u8, // Reserved for expansion of BSS properties + basic_mcs: [16]u8, // 802.11N BSS required MCS set + ie_offset: u16, // offset at which IEs start, from beginning + ie_length: u32, // byte length of Information Elements + snr: u16, // Average SNR(signal to noise ratio) during frame reception + // Variable-length Information Elements follow, see cyw43_ll_wifi_parse_scan_result +}; + +// zig fmt: off + +// Async events +const EventType = enum(u32) { + none = 0xffffffff, + set_ssid = 0, // indicates status of set ssid , + join = 1, // differentiates join ibss from found (wlc_e_start) ibss + start = 2, // sta founded an ibss or ap started a bss + auth = 3, // 802.11 auth request + auth_ind = 4, // 802.11 auth indication + deauth = 5, // 802.11 deauth request + deauth_ind = 6, // 802.11 deauth indication + assoc = 7, // 802.11 assoc request + assoc_ind = 8, // 802.11 assoc indication + reassoc = 9, // 802.11 reassoc request + reassoc_ind = 10, // 802.11 reassoc indication + disassoc = 11, // 802.11 disassoc request + disassoc_ind = 12, // 802.11 disassoc indication + quiet_start = 13, // 802.11h quiet period started + quiet_end = 14, // 802.11h quiet period ended + beacon_rx = 15, // beacons received/lost indication + link = 16, // generic link indication + mic_error = 17, // tkip mic error occurred + ndis_link = 18, // ndis style link indication + roam = 19, // roam attempt occurred: indicate status & reason + txfail = 20, // change in dot11failedcount (txfail) + pmkid_cache = 21, // wpa2 pmkid cache indication + retrograde_tsf = 22, // current ap's tsf value went backward + prune = 23, // ap was pruned from join list for reason + autoauth = 24, // report autoauth table entry match for join attempt + eapol_msg = 25, // event encapsulating an eapol message + scan_complete = 26, // scan results are ready or scan was aborted + addts_ind = 27, // indicate to host addts fail/success + delts_ind = 28, // indicate to host delts fail/success + bcnsent_ind = 29, // indicate to host of beacon transmit + bcnrx_msg = 30, // send the received beacon up to the host + bcnlost_msg = 31, // indicate to host loss of beacon + roam_prep = 32, // before attempting to roam + pfn_net_found = 33, // pfn network found event + pfn_net_lost = 34, // pfn network lost event + reset_complete = 35, + join_start = 36, + roam_start = 37, + assoc_start = 38, + ibss_assoc = 39, + radio = 40, + psm_watchdog = 41, // psm microcode watchdog fired + ccx_assoc_start = 42, // ccx association start + ccx_assoc_abort = 43, // ccx association abort + probreq_msg = 44, // probe request received + scan_confirm_ind = 45, + psk_sup = 46, // wpa handshake + country_code_changed = 47, + exceeded_medium_time = 48, // wmmac excedded medium time + icv_error = 49, // wep icv error occurred + unicast_decode_error = 50, // unsupported unicast encrypted frame + multicast_decode_error = 51, // unsupported multicast encrypted frame + trace = 52, + bta_hci_event = 53, // bt-amp hci event + i_f = 54, // i/f change (for wlan host notification) + p2p_disc_listen_complete = 55, // p2p discovery listen state expires + rssi = 56, // indicate rssi change based on configured levels + pfn_best_batching = 57, // pfn best network batching event + extlog_msg = 58, + action_frame = 59, // action frame reception + action_frame_complete = 60, // action frame tx complete + pre_assoc_ind = 61, // assoc request received + pre_reassoc_ind = 62, // re-assoc request received + channel_adopted = 63, // channel adopted (xxx: obsoleted) + ap_started = 64, // ap started + dfs_ap_stop = 65, // ap stopped due to dfs + dfs_ap_resume = 66, // ap resumed due to dfs + wai_sta_event = 67, // wai stations event + wai_msg = 68, // event encapsulating an wai message + escan_result = 69, // escan result event + action_frame_off_chan_complete = 70, // action frame off channel complete /* note - this used to be wlc_e_wake_event + probresp_msg = 71, // probe response received + p2p_probreq_msg = 72, // p2p probe request received + dcs_request = 73, + fifo_credit_map = 74, // credits for d11 fifos. [ac0,ac1,ac2,ac3,bc_mc,atim] + action_frame_rx = 75, // received action frame event with wl_event_rx_frame_data_t header + wake_event = 76, // wake event timer fired, used for wake wlan test mode + rm_complete = 77, // radio measurement complete + htsfsync = 78, // synchronize tsf with the host + overlay_req = 79, // request an overlay ioctl/iovar from the host + csa_complete_ind = 80, + excess_pm_wake_event = 81, // excess pm wake event to inform host + pfn_scan_none = 82, // no pfn networks around + pfn_scan_allgone = 83, // last found pfn network gets lost + gtk_plumbed = 84, + assoc_ind_ndis = 85, // 802.11 assoc indication for ndis only + reassoc_ind_ndis = 86, // 802.11 reassoc indication for ndis only + assoc_req_ie = 87, + assoc_resp_ie = 88, + assoc_recreated = 89, // association recreated on resume + action_frame_rx_ndis = 90, // rx action frame event for ndis only + auth_req = 91, // authentication request received + tdls_peer_event = 92, // discovered peer, connected/disconnected peer + //mesh_dhcp_success = 92, // dhcp handshake successful for a mesh interface + speedy_recreate_fail = 93, // fast assoc recreation failed + native = 94, // port-specific event and payload (e.g. ndis) + pktdelay_ind = 95, // event for tx pkt delay suddently jump + awdl_aw = 96, // awdl aw period starts + awdl_role = 97, // awdl master/slave/ne master role event + awdl_event = 98, // generic awdl event + nic_af_txs = 99, // nic af txstatus + nan = 100, // nan event + beacon_frame_rx = 101, + service_found = 102, // desired service found + gas_fragment_rx = 103, // gas fragment received + gas_complete = 104, // gas sessions all complete + p2po_add_device = 105, // new device found by p2p offload + p2po_del_device = 106, // device has been removed by p2p offload + wnm_sta_sleep = 107, // wnm event to notify sta enter sleep mode + txfail_thresh = 108, // indication of mac tx failures (exhaustion of 802.11 retries) exceeding threshold(s) + proxd = 109, // proximity detection event + ibss_coalesce = 110, // ibss coalescing + //mesh_paired = 110, // mesh peer found and paired + awdl_rx_prb_resp = 111, // awdl rx probe response + awdl_rx_act_frame = 112, // awdl rx action frames + awdl_wowl_nullpkt = 113, // awdl wowl nulls + awdl_phycal_status = 114, // awdl phycal status + awdl_oob_af_status = 115, // awdl oob af status + awdl_scan_status = 116, // interleaved scan status + awdl_aw_start = 117, // awdl aw start + awdl_aw_end = 118, // awdl aw end + awdl_aw_ext = 119, // awdl aw extensions + awdl_peer_cache_control = 120, + csa_start_ind = 121, + csa_done_ind = 122, + csa_failure_ind = 123, + cca_chan_qual = 124, // cca based channel quality report + bssid = 125, // to report change in bssid while roaming + tx_stat_error = 126, // tx error indication + bcmc_credit_support = 127, // credit check for bcmc supported + psta_primary_intf_ind = 128, // psta primary interface indication + bt_wifi_handover_req = 130, // handover request initiated + spw_txinhibit = 131, // southpaw txinhibit notification + fbt_auth_req_ind = 132, // fbt authentication request indication + rssi_lqm = 133, // enhancement addition for wlc_e_rssi + pfn_gscan_full_result = 134, // full probe/beacon (ies etc) results + pfn_swc = 135, // significant change in rssi of bssids being tracked + authorized = 136, // a sta been authroized for traffic + probreq_msg_rx = 137, // probe req with wl_event_rx_frame_data_t header + pfn_scan_complete = 138, // pfn completed scan of network list + rmc_event = 139, // rmc event + dpsta_intf_ind = 140, // dpsta interface indication + rrm = 141, // rrm event + ulp = 146, // ulp entry event + + tko = 151, // TCP Keep Alive Offload Event + ext_auth_req = 187, // authentication request received + ext_auth_frame_rx = 188, // authentication request received + mgmt_frame_txstatus = 189, // mgmt frame Tx complete + _, +}; + +// zig fmt: on + +pub const EventStatus = enum(u32) { + /// operation was successful + success = 0, + /// operation failed + fail = 1, + /// operation timed out + timeout = 2, + /// failed due to no matching network found + no_networks = 3, + /// operation was aborted + abort = 4, + /// protocol failure: packet not ack'd + no_ack = 5, + /// AUTH or ASSOC packet was unsolicited + unsolicited = 6, + /// attempt to assoc to an auto auth configuration + attempt = 7, + /// scan results are incomplete + partial = 8, + /// scan aborted by another scan + newscan = 9, + /// scan aborted due to assoc in progress + newassoc = 10, + /// 802.11h quiet period started + hquiet = 11, + /// user disabled scanning (WLC_SET_SCANSUPPRESS) + suppress = 12, + /// no allowable channels to scan + nochans = 13, + /// scan aborted due to CCX fast roam + ccxfastrm = 14, + /// abort channel select + cs_abort = 15, + _, +}; + +test "pwd encode" { + var buf: [36]u8 = @splat(0); + + try testing.expectEqualSlices( + u8, + &hex_to_bytes("0A0001005065726F5A6465726F31"), + encode_pwd(&buf, "PeroZdero1"), + ); + + try testing.expectEqualSlices( + u8, + &hex_to_bytes("080000006E696E617A617261"), + encode_ssid(&buf, "ninazara"), + ); +} + +pub fn encode_pwd(buf: []u8, pwd: []const u8) []u8 { + mem.writeInt(u32, buf[0..4], @intCast(pwd.len | 0x10000), .little); + @memcpy(buf[4..][0..pwd.len], pwd); + return buf[0 .. 4 + pwd.len]; +} + +pub fn encode_sae_pwd(buf: []u8, pwd: []const u8) []u8 { + mem.writeInt(u16, buf[0..2], @intCast(pwd.len), .little); + @memcpy(buf[2..][0..pwd.len], pwd); + return buf[0 .. 2 + pwd.len]; +} + +pub fn encode_ssid(buf: []u8, ssid: []const u8) []u8 { + mem.writeInt(u32, buf[0..4], @intCast(ssid.len), .little); + @memcpy(buf[4..][0..ssid.len], ssid); + return buf[0 .. 4 + ssid.len]; +} + +pub fn bytes_to_words(bytes: []const u8) struct { []const u32, ?u32 } { + const padding_bytes = bytes.len & 0b11; + const round_len = bytes.len - padding_bytes; + const words: []const u32 = + if (round_len > 0) + @alignCast(mem.bytesAsSlice(u32, @constCast(bytes)[0..round_len])) + else + &.{}; + + var padding_word: u32 = 0; + for (0..padding_bytes) |i| { + const b = bytes[bytes.len - 1 - i]; + padding_word = (padding_word << 8) | b; + } + return .{ words, if (padding_bytes == 0) null else padding_word }; +} + +test bytes_to_words { + { + const hex = hex_to_bytes("aabbcc"); + var bytes: [hex.len]u8 align(4) = undefined; + @memcpy(&bytes, &hex); + + const words, const padding = bytes_to_words(&bytes); + try testing.expectEqual(0, words.len); + try testing.expectEqual(0xccbbaa, padding.?); + } + { + const hex = hex_to_bytes("aabbccddeeff1122334455"); + var bytes: [hex.len]u8 align(4) = undefined; + @memcpy(&bytes, &hex); + + const words, const padding = bytes_to_words(&bytes); + try testing.expectEqualSlices(u32, &.{ 0xddccbbaa, 0x2211ffee }, words); + try testing.expectEqual(0x554433, padding.?); + } +} + +test "unaligned" { + const hex = hex_to_bytes("aabbccddeeff1122334455"); + const bytes = hex[0..5]; + + // var bytes2: []u8 = undefined; + // bytes2.ptr = @constCast(bytes.ptr); + // bytes2.len = 8; + // const words: []u32 = @alignCast(mem.bytesAsSlice(u32, @constCast(bytes2))); + + var words: []u32 = undefined; + words.ptr = @ptrCast(@alignCast(@constCast(bytes.ptr))); + words.len = 2; + + try testing.expectEqualSlices(u32, &.{ 0xddccbbaa, 0x2211ffee }, words); +} + +test "small data is padded in request to 4 bytes" { + var buf: [1024]u8 = undefined; + + const r1 = request(&buf, .set_var, "ampdu_ba_wsize", &.{ 0x08, 0, 0, 0 }, 1, 2); + const r2 = request(buf[512..], .set_var, "ampdu_ba_wsize", &.{0x08}, 1, 2); + + try testing.expectEqualSlices(u8, r1, r2); +} diff --git a/drivers/wireless/cyw43439/nvram.zig b/drivers/wireless/cyw43439/nvram.zig new file mode 100644 index 000000000..a67047483 --- /dev/null +++ b/drivers/wireless/cyw43439/nvram.zig @@ -0,0 +1,48 @@ +pub const NVRAM = + "NVRAMRev=$Rev$\x00" ++ + "manfid=0x2d0\x00" ++ + "prodid=0x0727\x00" ++ + "vendid=0x14e4\x00" ++ + "devid=0x43e2\x00" ++ + "boardtype=0x0887\x00" ++ + "boardrev=0x1100\x00" ++ + "boardnum=22\x00" ++ + "macaddr=00:A0:50:b5:59:5e\x00" ++ + "sromrev=11\x00" ++ + "boardflags=0x00404001\x00" ++ + "boardflags3=0x04000000\x00" ++ + "xtalfreq=37400\x00" ++ + "nocrc=1\x00" ++ + "ag0=255\x00" ++ + "aa2g=1\x00" ++ + "ccode=ALL\x00" ++ + "pa0itssit=0x20\x00" ++ + "extpagain2g=0\x00" ++ + "pa2ga0=-168,6649,-778\x00" ++ + "AvVmid_c0=0x0,0xc8\x00" ++ + "cckpwroffset0=5\x00" ++ + "maxp2ga0=84\x00" ++ + "txpwrbckof=6\x00" ++ + "cckbw202gpo=0\x00" ++ + "legofdmbw202gpo=0x66111111\x00" ++ + "mcsbw202gpo=0x77711111\x00" ++ + "propbw202gpo=0xdd\x00" ++ + "ofdmdigfilttype=18\x00" ++ + "ofdmdigfilttypebe=18\x00" ++ + "papdmode=1\x00" ++ + "papdvalidtest=1\x00" ++ + "pacalidx2g=45\x00" ++ + "papdepsoffset=-30\x00" ++ + "papdendidx=58\x00" ++ + "ltecxmux=0\x00" ++ + "ltecxpadnum=0x0102\x00" ++ + "ltecxfnsel=0x44\x00" ++ + "ltecxgcigpio=0x01\x00" ++ + "il0macaddr=00:90:4c:c5:12:38\x00" ++ + "wl0id=0x431b\x00" ++ + "deadman_to=0xffffffff\x00" ++ + "muxenab=0x100\x00" ++ + "spurconfig=0x3\x00" ++ + "glitch_based_crsmin=1\x00" ++ + "btc_mode=1\x00" ++ + "\x00"; diff --git a/drivers/wireless/cyw43439/wifi.zig b/drivers/wireless/cyw43439/wifi.zig new file mode 100644 index 000000000..c8351511a --- /dev/null +++ b/drivers/wireless/cyw43439/wifi.zig @@ -0,0 +1,702 @@ +const std = @import("std"); +const NVRAM = @import("nvram.zig").NVRAM; +const Bus = @import("bus.zig"); +const ioctl = @import("ioctl.zig"); +const assert = std.debug.assert; +const mem = std.mem; + +const log = std.log.scoped(.cyw43_wifi); + +const Self = @This(); + +bus: *Bus, +request_id: u16 = 0, +credit: u8 = 0, +tx_sequence: u8 = 0, +status: ?Status = null, +log_state: LogState = .{}, + +const ioctl_request_bytes_len = 1024; + +pub fn init(self: *Self) !void { + const bus = self.bus; + + // Init ALP (Active Low Power) clock + { + _ = bus.write_int(u8, .backplane, Bus.backplane.chip_clock_csr, Bus.backplane.alp_avail_req); + _ = bus.write_int(u8, .backplane, Bus.backplane.function2_watermark, 0x10); + const watermark = bus.read_int(u8, .backplane, Bus.backplane.function2_watermark); + if (watermark != 0x10) { + log.err("unexpected watermark {x}", .{watermark}); + return error.Cyw43Watermark; + } + // waiting for clock... + while (bus.read_int(u8, .backplane, Bus.backplane.chip_clock_csr) & Bus.backplane.alp_avail == 0) {} + // clear request for ALP + bus.write_int(u8, .backplane, Bus.backplane.chip_clock_csr, 0); + + // const chip_id = bus.read_int(u16, .backplane, chip.pmu_base_address); + // log.debug("chip ID: 0x{X}", .{chip_id}); + } + // Upload firmware + { + self.core_disable(.wlan); + self.core_reset(.socram); + + // this is 4343x specific stuff: Disable remap for SRAM_3 + bus.write_int(u32, .backplane, chip.socsram_base_address + 0x10, 3); + bus.write_int(u32, .backplane, chip.socsram_base_address + 0x44, 0); + + const firmware = @embedFile("../cyw43/firmware/43439A0_7_95_61.bin"); + bus.backplane_write(chip.atcm_ram_base_address, firmware); + } + // Load nvram + { + const nvram_len = ((NVRAM.len + 3) >> 2) * 4; // Round up to 4 bytes. + const addr_magic = chip.atcm_ram_base_address + chip.chip_ram_size - 4; + const addr = addr_magic - nvram_len; + bus.backplane_write(addr, NVRAM); + + const nvram_len_words = (nvram_len >> 2); + const nvram_len_magic = (~nvram_len_words << 16) | nvram_len_words; + bus.write_int(u32, .backplane, addr_magic, nvram_len_magic); + } + // starting up core... + self.core_reset(.wlan); + try self.core_is_up(.wlan); + + // wait until HT clock is available; takes about 29ms + while (bus.read_int(u8, .backplane, Bus.backplane.chip_clock_csr) & 0x80 == 0) {} + + // "Set up the interrupt mask and enable interrupts" + const sdio_int_host_mask: u32 = 0x24; + const i_hmb_sw_mask: u32 = 0x000000f0; + bus.write_int( + u32, + .backplane, + chip.sdiod_core_base_address + sdio_int_host_mask, + i_hmb_sw_mask, + ); + bus.write_int(u16, .bus, Bus.reg.interrupt_enable, @bitCast(Bus.Irq{ .f2_packet_available = true })); + + // "Lower F2 Watermark to avoid DMA Hang in F2 when SD Clock is stopped." + bus.write_int(u8, .backplane, Bus.backplane.function2_watermark, 0x20); + + // waiting for F2 to be ready... + while (true) { + const status: Status = @bitCast(self.bus.read_int(u32, .bus, Bus.reg.status)); + if (status.f2_rx_ready) break; + } + + // clear pad pulls + bus.write_int(u8, .backplane, Bus.backplane.pull_up, 0); + _ = bus.read_int(u8, .backplane, Bus.backplane.pull_up); + + // start HT clock + bus.write_int(u8, .backplane, Bus.backplane.chip_clock_csr, 0x10); + while (bus.read_int(u8, .backplane, Bus.backplane.chip_clock_csr) & 0x80 == 0) {} + + // Load Country Locale Matrix (CLM) + { + const data = @embedFile("../cyw43/firmware/43439A0_clm.bin"); + + const ClmLoadControl = extern struct { + flag: u16 = 0, + typ: u16 = 2, + len: u32 = 0, + crc: u32 = 0, + }; + + var nbytes: usize = 0; + while (nbytes < data.len) { + const head_len = @sizeOf(ClmLoadControl); + var clr: ClmLoadControl = .{}; + var chunk: [ioctl_request_bytes_len - 64]u8 = undefined; + const n = @min(chunk.len - head_len, data.len - nbytes); + clr.flag = 1 << 12 | (if (nbytes > 0) @as(u16, 0) else 2) | (if (nbytes + n >= data.len) @as(u16, 4) else 0); + clr.len = n; + @memcpy(chunk[0..head_len], mem.asBytes(&clr)); + @memcpy(chunk[head_len..][0..n], data[nbytes..][0..n]); + try self.set_var("clmload", chunk[0 .. head_len + n]); + nbytes += n; + } + } + self.log_init(); +} + +fn core_disable(self: *Self, core: Core) void { + const base = core.base_addr(); + // Dummy read? + _ = self.bus.read_int(u8, .backplane, base + ai.resetctrl_offset); + // Check it isn't already reset + const r = self.bus.read_int(u8, .backplane, base + ai.resetctrl_offset); + if (r & ai.resetctrl_bit_reset != 0) { + return; + } + self.bus.write_int(u8, .backplane, base + ai.ioctrl_offset, 0); + _ = self.bus.read_int(u8, .backplane, base + ai.ioctrl_offset); + self.sleep_ms(1); + self.bus.write_int(u8, .backplane, base + ai.resetctrl_offset, ai.resetctrl_bit_reset); + _ = self.bus.read_int(u8, .backplane, base + ai.resetctrl_offset); +} + +fn core_reset(self: *Self, core: Core) void { + self.core_disable(core); + const base = core.base_addr(); + self.bus.write_int(u8, .backplane, base + ai.ioctrl_offset, ai.ioctrl_bit_fgc | ai.ioctrl_bit_clock_en); + _ = self.bus.read_int(u8, .backplane, base + ai.ioctrl_offset); + self.bus.write_int(u8, .backplane, base + ai.resetctrl_offset, 0); + self.sleep_ms(1); + self.bus.write_int(u8, .backplane, base + ai.ioctrl_offset, ai.ioctrl_bit_clock_en); + _ = self.bus.read_int(u8, .backplane, base + ai.ioctrl_offset); + self.sleep_ms(1); +} + +fn core_is_up(self: *Self, core: Core) !void { + const base = core.base_addr(); + const io = self.bus.read_int(u8, .backplane, base + ai.ioctrl_offset); + if (io & (ai.ioctrl_bit_fgc | ai.ioctrl_bit_clock_en) != ai.ioctrl_bit_clock_en) { + log.err("core_is_up fail due to bad ioctrl 0x{X}", .{io}); + return error.Cyw43CoreIsUp; + } + const r = self.bus.read_int(u8, .backplane, base + ai.resetctrl_offset); + if (r & (ai.resetctrl_bit_reset) != 0) { + log.err("core_is_up fail due to bad resetctrl 0x{X}", .{r}); + return error.Cyw43CoreIsUp; + } +} + +fn log_init(self: *Self) void { + const addr = chip.atcm_ram_base_address + chip.chip_ram_size - 4 - chip.socram_srmem_size; + const shared_addr = self.bus.read_int(u32, .backplane, addr); + var shared: SharedMemData = undefined; + self.bus.backplane_read(shared_addr, std.mem.asBytes(&shared)); + self.log_state.addr = shared.console_addr + 8; +} + +pub fn log_read(self: *Self) void { + const chip_log = std.log.scoped(.cyw43_chip); + + var shared: SharedMemLog = undefined; + self.bus.backplane_read(self.log_state.addr, std.mem.asBytes(&shared)); + if (shared.idx == self.log_state.idx) return; + + var buf: [1024]u8 align(4) = undefined; + self.bus.backplane_read(shared.buf, &buf); + if (shared.idx == 0 or buf[shared.idx - 1] != '\n') return; + + const tail = if (shared.idx < self.log_state.idx) buf.len else shared.idx; + var iter = mem.splitAny(u8, buf[self.log_state.idx..tail], &.{ '\r', '\n' }); + while (iter.next()) |line| { + if (line.len == 0) continue; + chip_log.debug("{s}", .{line}); + } + self.log_state.idx = if (tail == buf.len) 0 else tail; +} + +pub fn set_var(self: *Self, name: []const u8, data: []const u8) !void { + self.request(.set_var, name, data); + _ = try self.response_poll(.set_var, null); +} + +pub fn set_cmd(self: *Self, cmd: ioctl.Cmd, data: []const u8) !void { + self.request(cmd, "", data); + _ = try self.response_poll(cmd, null); +} + +pub fn get_var(self: *Self, name: []const u8, data: []u8) !usize { + self.request(.get_var, name, data); + return try self.response_poll(.get_var, data); +} + +fn request(self: *Self, cmd: ioctl.Cmd, name: []const u8, data: []const u8) void { + self.request_id +%= 1; + self.tx_sequence +%= 1; + var words: [ioctl_request_bytes_len / 4]u32 = undefined; + const bytes = ioctl.request( + mem.sliceAsBytes(words[1..]), // 1 word reserved for ioctl cmd + cmd, + name, + data, + self.request_id, + self.tx_sequence, + ); + const words_len = ((bytes.len + 3) >> 2) + 1; + self.bus.write(.wlan, 0, @intCast(bytes.len), words[0..words_len]); +} + +fn response_poll(self: *Self, cmd: ioctl.Cmd, data: ?[]u8) !usize { + var bytes: [ioctl_request_bytes_len]u8 align(4) = undefined; + var delay: usize = 0; + while (delay < ioctl.response_wait) { + const rsp = try self.read(&bytes) orelse { + self.sleep_ms(ioctl.response_poll_interval); + delay += ioctl.response_poll_interval; + continue; + }; + switch (rsp.sdp.channel()) { + .control => { + const cdc = rsp.cdc(); + if (cdc.id == self.request_id) { + if (cdc.cmd == cmd and cdc.status_ok()) { + if (data) |d| { + const rsp_data = rsp.data(); + const n = @min(rsp_data.len, d.len); + @memcpy(d[0..n], rsp_data[0..n]); + return n; + } + return 0; + } + self.log_response(rsp); + return error.Cyw43InvalidCommandStatus; + } + }, + else => self.log_response(rsp), + } + } + log.err("ioctl: missing response in response_poll", .{}); + return error.Cyw43NoResponse; +} + +pub const JoinOptions = struct { + security: Security = .wpa2_psk, + country: Country = .{}, + + pub const Security = enum { + open, + wpa_psk, + wpa2_psk, + wpa3_sae, + }; + + // ref: https://github.com/embassy-rs/embassy/blob/96a026c73bad2ebb8dfc78e88c9690611bf2cb97/cyw43/src/structs.rs#L371 + pub const Country = struct { + code: [2]u8 = "XX".*, // Worldwide + revision: i32 = -1, + }; +}; + +pub fn join(self: *Self, ssid: []const u8, pwd: []const u8, opt: JoinOptions) !void { + const bus = self.bus; + + // Clear pullups + { + bus.write_int(u8, .backplane, Bus.backplane.pull_up, 0xf); + bus.write_int(u8, .backplane, Bus.backplane.pull_up, 0); + _ = self.bus.read_int(u8, .backplane, Bus.backplane.pull_up); + } + // Clear data unavail error + { + const val = self.bus.read_int(u16, .bus, Bus.reg.interrupt); + if (val & 1 > 0) + self.bus.write_int(u16, .bus, Bus.reg.interrupt, val); + } + // Set sleep KSO (should poll to check for success) + { + bus.write_int(u8, .backplane, Bus.backplane.sleep_csr, 1); + bus.write_int(u8, .backplane, Bus.backplane.sleep_csr, 1); + _ = self.bus.read_int(u8, .backplane, Bus.backplane.pull_up); + //log.debug("REG_BACKPLANE_SLEEP_CSR value: {}", .{val}); + } + // Set country + { + const code = opt.country.code; + var val = extern struct { + abbrev: [4]u8, + revision: i32, + code: [4]u8, + }{ + .abbrev = .{ code[0], code[1], 0, 0 }, + .revision = opt.country.revision, + .code = .{ code[0], code[1], 0, 0 }, + }; + self.set_var("country", mem.asBytes(&val)) catch |err| switch (err) { + error.Cyw43InvalidCommandStatus => { + log.err( + "invalid country code: {s}, revision: {}", + .{ opt.country.code, opt.country.revision }, + ); + return error.Cyw43InvalidCountryCode; + }, + else => return err, + }; + } + try self.set_cmd(.set_antdiv, &.{0}); + // Data aggregation + { + try self.set_var("bus:txglom", &.{0x00}); + try self.set_var("apsta", &.{0x01}); + try self.set_var("ampdu_ba_wsize", &.{0x08}); + try self.set_var("ampdu_mpdu", &.{0x04}); + try self.set_var("ampdu_rx_factor", &.{0x00}); + self.sleep_ms(150); + } + // Enable events + { + // using events list from: https://github.com/jbentham/picowi/blob/bb33b1e7a15a685f06dda6764b79e429ce9b325e/lib/picowi_join.c#L38 + // ref: https://github.com/jbentham/picowi/blob/bb33b1e7a15a685f06dda6764b79e429ce9b325e/lib/picowi_join.c#L74 + // can be something like: + // ref: https://github.com/embassy-rs/embassy/blob/96a026c73bad2ebb8dfc78e88c9690611bf2cb97/cyw43/src/control.rs#L242 + const buf = ioctl.hex_to_bytes("000000008B120102004000000000800100000000000000000000"); + try self.set_var("bsscfg:event_msgs", &buf); + self.sleep_ms(50); + } + var buf: [64]u8 = @splat(0); // space for 10 addresses + // Enable multicast + { + @memcpy(buf[0..4], &[_]u8{ 0x01, 0x00, 0x00, 0x00 }); // number of addresses + @memcpy(buf[4..][0..6], &[_]u8{ 0x01, 0x00, 0x5E, 0x00, 0x00, 0xFB }); // address + try self.set_var("mcast_list", &buf); + self.sleep_ms(50); + } + // join_restart function + { + try self.set_cmd(.up, &.{}); + try self.set_cmd(.set_gmode, &.{1}); + try self.set_cmd(.set_band, &.{0}); + try self.set_var("pm2_sleep_ret", &.{0xc8}); + try self.set_var("bcn_li_bcn", &.{1}); + try self.set_var("bcn_li_dtim", &.{1}); + try self.set_var("assoc_listen", &.{0x0a}); + + try self.set_cmd(.set_wsec, &.{switch (opt.security) { + .wpa_psk => 2, + .wpa2_psk, .wpa3_sae => 6, + .open => 0, + }}); + switch (opt.security) { + .open => { + try self.set_var("bsscfg:sup_wpa", &.{ 0, 0, 0, 0, 0, 0, 0, 0 }); + }, + else => { + try self.set_var("bsscfg:sup_wpa", &.{ 0, 0, 0, 0, 1, 0, 0, 0 }); + try self.set_var("bsscfg:sup_wpa2_eapver", &.{ 0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff }); + try self.set_var("bsscfg:sup_wpa_tmo", &.{ 0, 0, 0, 0, 0xc4, 0x09, 0, 0 }); + }, + } + self.sleep_ms(2); + + switch (opt.security) { + .open => {}, + .wpa3_sae => { + try self.set_var("sae_password", ioctl.encode_sae_pwd(&buf, pwd)); + }, + else => { + try self.set_cmd(.set_wsec_pmk, ioctl.encode_pwd(&buf, pwd)); + }, + } + try self.set_cmd(.set_infra, &.{1}); + + try self.set_cmd(.set_auth, &.{switch (opt.security) { + .wpa3_sae => 3, + else => 0, + }}); + try self.set_var("mfp", &.{switch (opt.security) { + .wpa_psk => 0, + .wpa2_psk => 1, + .wpa3_sae => 2, + .open => 0, + }}); + try self.set_cmd(.set_wpa_auth, switch (opt.security) { + .wpa_psk => &.{ 0x04, 0, 0, 0 }, + .wpa2_psk => &.{ 0x80, 0, 0, 0 }, + .wpa3_sae => &.{ 0, 0, 0x04, 0 }, + .open => &.{ 0, 0, 0, 0 }, + }); + + try self.set_cmd(.set_ssid, ioctl.encode_ssid(&buf, ssid)); + } + + try self.join_wait(30 * 1000, opt.security); +} + +fn join_wait(self: *Self, wait_ms: u32, security: JoinOptions.Security) !void { + var delay: u32 = 0; + var link_up: bool = false; + var link_auth: bool = security == .open; + var set_ssid: bool = false; + var bytes: [512]u8 align(4) = undefined; + + while (delay < wait_ms) { + // self.log_read(); + const rsp = try self.read(&bytes) orelse { + self.sleep_ms(ioctl.response_poll_interval); + delay += ioctl.response_poll_interval; + continue; + }; + switch (rsp.sdp.channel()) { + .event => { + const evt = rsp.event().msg; + // log.debug( + // " event type: {s:<15}, status: {s} flags: {x}", + // .{ @tagName(evt.event_type), @tagName(evt.status), evt.flags }, + // ); + switch (evt.event_type) { + .link => { + if (evt.flags & 1 == 0) return error.Cyw43JoinLinkDown; + link_up = true; + }, + .psk_sup => { + if (evt.status != .unsolicited) return error.Cyw43JoinWpaHandshake; + link_auth = true; + }, + .assoc => { + if (evt.status != .success) return error.Cyw43JoinAssocRequest; + }, + .auth => { + if (evt.status != .success) return error.Cyw43JoinAuthRequest; + }, + .disassoc_ind => { + return error.Cyw43JoinDisassocIndication; + }, + .set_ssid => { + if (evt.status != .success) return error.Cyw43JoinSetSsid; + set_ssid = true; + }, + else => {}, + } + if (set_ssid and link_up and link_auth) { + //log.debug("join OK", .{}); + return; + } + }, + else => self.log_response(rsp), + } + } + return error.Cyw43JoinTimeout; +} + +// show unexpected command response +// can be assert also +fn log_response(self: *Self, rsp: ioctl.Response) void { + _ = self; + switch (rsp.sdp.channel()) { + .event => { + const evt = rsp.event().msg; + if (evt.event_type == .none and evt.status == .success) + return; + log.info( + "unhandled event type: {}, status: {} ", + .{ evt.event_type, evt.status }, + ); + }, + .control => { + log.err("unexpected command response:", .{}); + log.err(" bus: {}", .{rsp.sdp}); + log.err(" cdc: {}", .{rsp.cdc()}); + log.err(" data: {x}", .{rsp.data()}); + }, + .data => { + log.err("unexpected data:", .{}); + log.err(" bus: {}", .{rsp.sdp}); + log.err(" bdc: {}", .{rsp.bdc()}); + log.err(" data: {x}", .{rsp.data()}); + }, + } +} + +fn sleep_ms(self: *Self, delay: u32) void { + self.bus.sleep_ms(delay); +} + +/// buffer.len should be 1540 bytes. Last 4 bytes are for the status which is +/// writen after each read. 22 bytes at the start is bus header, 18 bytes header +/// + 4 bytes padding. After that is layer 2, ethernet, packet; 14 bytes of +/// ethernet header and 1500 bytes of layer 3 mtu. +/// +/// 22 bytes bus header (18 + padding) +/// 14 bytes ethernet header +/// 1500 bytes layer 3 mtu +/// 4 bytes status +/// 1540 bytes total +/// +/// Layer 2 header+payload position is passed to the caller. First return +/// argument is start of that data in the buffer second is length. +/// +pub fn recv_zc(self: *Self, buffer: []u8) !?struct { usize, usize } { + while (true) { + const rsp = try self.read(buffer) orelse return null; + switch (rsp.sdp.channel()) { + .data => return rsp.data_pos(), + else => self.log_response(rsp), + } + } +} + +/// Buffer has to have 22 bytes headroom for ioctl command (4 bytes) and bus +/// header (18 bytes). After that layer 2 should be prepared, 14 bytes ethernet +/// header and up to 1500 bytes layer 3 mtu. +/// +/// 22 bytes headroom for ioctl command and bus header +/// 14 bytes ethernet header +/// 1500 bytes layer 3 mtu +/// 1536 bytes total +/// +/// Buffer has to be 4 bytes aligned and it will be extended in as_words to the +/// word boundary! +pub fn send_zc(self: *Self, buffer: []u8) !void { + if (!self.has_credit()) return error.Cyw43NoCredit; + + const eth_frame_len = buffer.len - 22; + // add bus header + self.tx_sequence +%= 1; + buffer[4..][0..18].* = ioctl.tx_header(@intCast(eth_frame_len), self.tx_sequence); + + // bus write + const bytes_len = 18 + eth_frame_len; + const words_len = ((bytes_len + 3) >> 2) + 1; // round and add 1 for bus command + + self.bus.write(.wlan, 0, @intCast(bytes_len), as_words(buffer, words_len)); +} + +pub fn has_credit(self: *Self) bool { + return self.tx_sequence != self.credit and (self.credit -% self.tx_sequence) & 0x80 == 0; +} + +// Read packet from the wifi chip. Assuming that this is used in the loop +// until it returns null. That way we can cache status from previous read. +fn read(self: *Self, buffer: []u8) !?ioctl.Response { + if (self.status == null) self.read_status(); + const status = self.status.?; + self.status = null; + if (status.f2_packet_available and status.packet_length > 0) { + const words_len: usize = ((status.packet_length + 3) >> 2) + 1; // add one word for the status + const words = as_words(buffer, words_len); + + self.bus.read(.wlan, 0, status.packet_length, words); + // last word is status + self.status = @bitCast(words[words.len - 1]); + // parse response + const rsp = try ioctl.response(mem.sliceAsBytes(words)[0..status.packet_length]); + // update credit + self.credit = rsp.sdp.credit; + return rsp; + } + return null; +} + +fn as_words(bytes: []u8, len: usize) []u32 { + var words: []u32 = undefined; + words.ptr = @ptrCast(@alignCast(@constCast(bytes.ptr))); + words.len = len; + return words; +} + +fn read_status(self: *Self) void { + self.status = @bitCast(self.bus.read_int(u32, .bus, Bus.reg.status)); +} + +pub fn gpio_enable(self: *Self, pin: u2) void { + self.bus.write_int(u32, .backplane, chip.gpio.enable, @as(u32, 1) << pin); +} + +pub fn gpio_toggle(self: *Self, pin: u2) void { + var val = self.bus.read_int(u32, .backplane, chip.gpio.output); + val = val ^ @as(u32, 1) << pin; + self.bus.write_int(u32, .backplane, chip.gpio.output, val); +} + +// to set gpio pin by sending command +pub fn gpio_out(self: *Self, pin: u2, on: bool) !void { + var data: [8]u8 = @splat(0); + data[0] = @as(u8, 1) << pin; + data[4] = if (on) 1 else 0; + try self.set_var("gpioout", &data); +} + +// ref: datasheet 'Table 5. gSPI Status Field Details' +const Status = packed struct { + data_not_available: bool, // The requested read data is not available. + underflow: bool, // FIFO underflow occurred due to current (F2, F3) read command. + overflow: bool, // FIFO overflow occurred due to current (F1, F2, F3) write command. + f2_interrupt: bool, // F2 channel interrupt. + _reserved1: u1, + f2_rx_ready: bool, // F2 FIFO is ready to receive data (FIFO empty). + _reserved2: u2, + f2_packet_available: bool, // Packet is available/ready in F2 TX FIFO. + packet_length: u11, // Length of packet available in F2 FIFO, + _reserved3: u12, +}; + +// CYW43439 chip values +const chip = struct { + const wrapper_register_offset: u32 = 0x100000; + + const arm_core_base_address: u32 = 0x18003000 + wrapper_register_offset; + const socsram_base_address: u32 = 0x18004000; + const bluetooth_base_address: u32 = 0x19000000; + const socsram_wrapper_base_address: u32 = 0x18004000 + wrapper_register_offset; + const sdiod_core_base_address: u32 = 0x18002000; + const pmu_base_address: u32 = 0x18000000; + const chip_ram_size: u32 = 512 * 1024; + const atcm_ram_base_address: u32 = 0; + const socram_srmem_size: u32 = 64 * 1024; + const chanspec_band_mask: u32 = 0xc000; + const chanspec_band_2g: u32 = 0x0000; + const chanspec_band_5g: u32 = 0xc000; + const chanspec_band_shift: u32 = 14; + const chanspec_bw_10: u32 = 0x0800; + const chanspec_bw_20: u32 = 0x1000; + const chanspec_bw_40: u32 = 0x1800; + const chanspec_bw_mask: u32 = 0x3800; + const chanspec_bw_shift: u32 = 11; + const chanspec_ctl_sb_lower: u32 = 0x0000; + const chanspec_ctl_sb_upper: u32 = 0x0100; + const chanspec_ctl_sb_none: u32 = 0x0000; + const chanspec_ctl_sb_mask: u32 = 0x0700; + + const gpio = struct { + pub const output = (pmu_base_address + 0x64); + pub const enable = (pmu_base_address + 0x68); + }; +}; + +// Broadcom AMBA (Advanced Microcontroller Bus Architecture) Interconnect +const ai = struct { + const ioctrl_offset: u32 = 0x408; + const ioctrl_bit_fgc: u8 = 0x0002; + const ioctrl_bit_clock_en: u8 = 0x0001; + const ioctrl_bit_cpuhalt: u8 = 0x0020; + + const resetctrl_offset: u32 = 0x800; + const resetctrl_bit_reset: u8 = 1; + + const resetstatus_offset: u32 = 0x804; +}; + +const Core = enum(u2) { + wlan = 0, + socram = 1, + sdiod = 2, + + fn base_addr(self: Core) u32 { + return switch (self) { + .wlan => chip.arm_core_base_address, + .socram => chip.socsram_wrapper_base_address, + .sdiod => chip.sdiod_core_base_address, + }; + } +}; + +const LogState = struct { + addr: u32 = 0, + idx: usize = 0, +}; + +const SharedMemData = extern struct { + flags: u32, + trap_addr: u32, + assert_exp_addr: u32, + assert_file_addr: u32, + assert_line: u32, + console_addr: u32, + msgtrace_addr: u32, + fwid: u32, +}; + +const SharedMemLog = extern struct { + buf: u32, + buf_size: u32, + idx: u32, + out_idx: u32, +}; diff --git a/examples/raspberrypi/rp2xxx/build.zig b/examples/raspberrypi/rp2xxx/build.zig index 05da4c690..8d411c86f 100644 --- a/examples/raspberrypi/rp2xxx/build.zig +++ b/examples/raspberrypi/rp2xxx/build.zig @@ -84,6 +84,7 @@ pub fn build(b: *std.Build) void { .{ .name = "ssd1306", .file = "src/ssd1306_oled.zig", .imports = &.{ .{ .name = "font8x8", .module = font8x8_dep.module("font8x8") }, } }, + .{ .name = "net-dhcp", .file = "src/net/dhcp.zig" }, }; var available_examples: std.array_list.Managed(Example) = .init(b.allocator); @@ -132,6 +133,28 @@ pub fn build(b: *std.Build) void { .imports = example.imports, }); + if (std.mem.indexOf(u8, example.name, "_net-") != null) { + const target = b.resolveTargetQuery(firmware.target.zig_target); + const foundation_dep = b.dependency("foundationlibc", .{ + .target = target, + .optimize = optimize, + }); + const lwip_dep = b.dependency("lwip", .{ + .target = target, + .optimize = optimize, + }); + const lwip_mod = lwip_dep.module("lwip"); + // link libc + lwip_mod.linkLibrary(foundation_dep.artifact("foundation")); + // add path to the configuration, lwipopts.h + lwip_mod.addIncludePath(b.path("src/net/lwip/include")); + // add c import paths + for (lwip_mod.include_dirs.items) |dir| { + firmware.app_mod.include_dirs.append(b.allocator, dir) catch @panic("out of memory"); + } + firmware.app_mod.addImport("lwip", lwip_mod); + } + // `install_firmware()` is the MicroZig pendant to `Build.installArtifact()` // and allows installing the firmware as a typical firmware file. // diff --git a/examples/raspberrypi/rp2xxx/build.zig.zon b/examples/raspberrypi/rp2xxx/build.zig.zon index 609b7a2c3..408c96a66 100644 --- a/examples/raspberrypi/rp2xxx/build.zig.zon +++ b/examples/raspberrypi/rp2xxx/build.zig.zon @@ -1,6 +1,7 @@ .{ .name = .examples_raspberrypi_rp2xxx, .fingerprint = 0xffa4bfa151162a57, + .minimum_zig_version = "0.15.1", .version = "0.0.0", .dependencies = .{ .microzig = .{ .path = "../../.." }, @@ -8,6 +9,8 @@ .url = "git+https://github.com/Gnyblast/zig-ssd1306-gddram-fonts#84dd5fab70ddc1198eef1cd1305cc82f08f419dc", .hash = "ssd1306_font_8x8-0.0.0-oGb_cERcAAA6NJzWDOMepgnY6r4blNEHjpkldDaRAui5", }, + .foundationlibc = .{ .path = "../../../modules/foundation-libc/" }, + .lwip = .{ .path = "../../../modules/lwip/" }, }, .paths = .{ "README.md", diff --git a/examples/raspberrypi/rp2xxx/src/cyw43.zig b/examples/raspberrypi/rp2xxx/src/cyw43.zig index 02fb063eb..8fe38ed4b 100644 --- a/examples/raspberrypi/rp2xxx/src/cyw43.zig +++ b/examples/raspberrypi/rp2xxx/src/cyw43.zig @@ -9,7 +9,7 @@ const gpio = rp2xxx.gpio; const pio = rp2xxx.pio; const drivers = microzig.hal.drivers; -const CYW43_Pio_Device = drivers.CYW43_Pio_Device; +var wifi_driver: drivers.WiFi = .{}; const uart = rp2xxx.uart.instance.num(0); const uart_tx_pin = gpio.num(0); @@ -19,6 +19,8 @@ pub const microzig_options = microzig.Options{ .logFn = rp2xxx.uart.log, }; +const log = std.log.scoped(.main); + pub fn main() !void { // init uart logging uart_tx_pin.set_function(.uart); @@ -27,19 +29,13 @@ pub fn main() !void { }); rp2xxx.uart.init_logger(uart); - const cyw43_config = drivers.CYW43_Pio_Device_Config{ - .spi = .{ - .pio = pio.num(0), - .cs_pin = gpio.num(25), - .io_pin = gpio.num(24), - .clk_pin = gpio.num(29), - }, - .pwr_pin = gpio.num(23), - }; - var cyw43: CYW43_Pio_Device = .{}; - try cyw43.init(cyw43_config); + // init cyw43 + var wifi = try wifi_driver.init(.{}); + log.debug("mac address: {x}", .{wifi.mac}); + var led = wifi.gpio(0); - // The driver isn't finished yet, so we're using this infinite test loop to process all internal driver events. - // Eventually, this will be replaced by a dedicated driver task/thread. - cyw43.test_loop(); + while (true) { + time.sleep_ms(500); + led.toggle(); + } } diff --git a/examples/raspberrypi/rp2xxx/src/net/dhcp.zig b/examples/raspberrypi/rp2xxx/src/net/dhcp.zig new file mode 100644 index 000000000..ae7006280 --- /dev/null +++ b/examples/raspberrypi/rp2xxx/src/net/dhcp.zig @@ -0,0 +1,75 @@ +const std = @import("std"); +const microzig = @import("microzig"); +const rp2xxx = microzig.hal; +const time = rp2xxx.time; +const gpio = rp2xxx.gpio; +const pio = rp2xxx.pio; +const drivers = rp2xxx.drivers; + +const uart = rp2xxx.uart.instance.num(0); +const uart_tx_pin = gpio.num(0); +pub const microzig_options = microzig.Options{ + .log_level = .debug, + .logFn = rp2xxx.uart.log, +}; +const log = std.log.scoped(.main); + +const Net = @import("lwip/net.zig"); +comptime { + _ = @import("lwip/exports.zig"); +} +const secrets = @import("secrets.zig"); + +pub fn main() !void { + // init uart logging + uart_tx_pin.set_function(.uart); + uart.apply(.{ + .clock_config = rp2xxx.clock_config, + }); + rp2xxx.uart.init_logger(uart); + + // init cyw43 + var wifi_driver: drivers.WiFi = .{}; + var wifi = try wifi_driver.init(.{}); + var led = wifi.gpio(0); + log.debug("mac address: {x}", .{wifi.mac}); + + // join network + try wifi.join(secrets.ssid, secrets.pwd, .{}); + log.debug("wifi joined", .{}); + + // init lwip + var net: Net = .{ + .mac = wifi.mac, + .link = .{ + .ptr = wifi, + .recv = drivers.WiFi.recv, + .send = drivers.WiFi.send, + .ready = drivers.WiFi.ready, + }, + }; + try net.init(); + + var ts = time.get_time_since_boot(); + while (true) { + // run lwip poller + try net.poll(); + + // blink + const now = time.get_time_since_boot(); + if (now.diff(ts).to_us() > 500_000) { + ts = now; + led.toggle(); + } + } +} + +// This will log dhcp assigned ip address, something like: +// +// ================ STARTING NEW LOGGER ================ +// [0.700113] debug (main): mac address: 2ccf67f3b7ea +// [4.382145] debug (main): wifi joined +// [5.095823] debug (lwip): netif status callback is_link_up: false, is_up: true, ready: false, ip: 0.0.0.0 +// [7.479941] debug (lwip): netif status callback is_link_up: true, is_up: true, ready: true, ip: 192.168.190.206 +// +// you can than ping that ip. diff --git a/examples/raspberrypi/rp2xxx/src/net/lwip/exports.zig b/examples/raspberrypi/rp2xxx/src/net/lwip/exports.zig new file mode 100644 index 000000000..76146a286 --- /dev/null +++ b/examples/raspberrypi/rp2xxx/src/net/lwip/exports.zig @@ -0,0 +1,45 @@ +/// Platform dependent exports required by lwip +/// +const std = @import("std"); +const microzig = @import("microzig"); +const rp2xxx = microzig.hal; +const time = rp2xxx.time; +const trng = rp2xxx.rand.trng; + +const log = std.log.scoped(.lwip); +const assert = std.debug.assert; + +export fn lwip_lock_interrupts(were_enabled: *bool) void { + _ = were_enabled; +} + +export fn lwip_unlock_interrupts(enable: bool) void { + _ = enable; +} + +export fn lwip_rand() u32 { + return if (rp2xxx.compatibility.chip == .RP2350) + trng.random_blocking() + else + 4; +} + +export fn lwip_assert(msg: [*c]const u8, file: [*c]const u8, line: c_int) void { + log.err("assert: {s} in file: {s}, line: {}", .{ msg, file, line }); + @panic("lwip assert"); +} + +export fn lwip_diag(msg: [*c]const u8, file: [*c]const u8, line: c_int) void { + log.debug("{s} in file: {s}, line: {}", .{ msg, file, line }); +} + +// required by fundation-libc/src/modules/errno.zig +// line 12 threadlocal +export fn __aeabi_read_tp() u32 { + return 0; +} + +export fn sys_now() u32 { + const ts = time.get_time_since_boot(); + return @truncate(ts.to_us() / 1000); +} diff --git a/examples/raspberrypi/rp2xxx/src/net/lwip/include/arch/cc.h b/examples/raspberrypi/rp2xxx/src/net/lwip/include/arch/cc.h new file mode 100644 index 000000000..3b7e19576 --- /dev/null +++ b/examples/raspberrypi/rp2xxx/src/net/lwip/include/arch/cc.h @@ -0,0 +1,44 @@ +#ifndef lwip__cc_h +#define lwip__cc_h + +#include +#include + +typedef unsigned int sys_prot_t; + +extern uint32_t lwip_rand(void); +extern void lwip_lock_interrupts(bool *state); +extern void lwip_unlock_interrupts(bool state); +extern void lwip_assert(const char *msg, const char *file, int line); +extern void lwip_diag(const char *msg, const char *file, int line); + +#define LWIP_PLATFORM_DIAG(x) \ + do { \ + lwip_diag((msg), __FILE__, __LINE__); \ + } while (0) +#define LWIP_PLATFORM_ASSERT(msg) \ + do { \ + lwip_assert((msg), __FILE__, __LINE__); \ + } while (0) + +#define BYTE_ORDER LITTLE_ENDIAN + +#define LWIP_RAND() ((u32_t)lwip_rand()) + +#define LWIP_NO_STDDEF_H 0 +#define LWIP_NO_STDINT_H 0 +#define LWIP_NO_INTTYPES_H 1 +#define LWIP_NO_LIMITS_H 0 +#define LWIP_NO_CTYPE_H 1 + +#define LWIP_UNUSED_ARG(x) (void)x +#define LWIP_PROVIDE_ERRNO 1 + +// Critical section support: +// https://www.nongnu.org/lwip/2_1_x/group__sys__prot.html + +#define SYS_ARCH_DECL_PROTECT(lev) bool lev +#define SYS_ARCH_PROTECT(lev) lwip_lock_interrupts(&lev) +#define SYS_ARCH_UNPROTECT(lev) lwip_unlock_interrupts(lev) + +#endif // lwip__cc_h diff --git a/examples/raspberrypi/rp2xxx/src/net/lwip/include/lwipopts.h b/examples/raspberrypi/rp2xxx/src/net/lwip/include/lwipopts.h new file mode 100644 index 000000000..1934d40aa --- /dev/null +++ b/examples/raspberrypi/rp2xxx/src/net/lwip/include/lwipopts.h @@ -0,0 +1,58 @@ +#ifndef lwipopts_h +#define lwipopts_h + +// freestanding envrionment +#define NO_SYS 1 // we don't have an OS +#define LWIP_SOCKET 0 // requires multi-threaded environments with os +#define LWIP_NETCONN 0 // requires multi-threaded environments with os +#define LWIP_MPU_COMPATIBLE 0 +#define LWIP_TCPIP_CORE_LOCKING 0 + +// features +#define LWIP_IPV4 1 +#define LWIP_IPV6 1 +#define LWIP_UDP 1 +#define LWIP_TCP 1 +#define LWIP_DHCP 1 +#define LWIP_IGMP LWIP_IPV4 +#define LWIP_ICMP LWIP_IPV4 +#define LWIP_DNS LWIP_UDP +#define LWIP_MDNS_RESPONDER LWIP_UDP + +// memory +#define MEM_ALIGNMENT 4 +#define MEM_SIZE (32 * 1024) // limit of dynamic mem alloc + +#define PBUF_POOL_SIZE 32 // number of preallocated buffers +#define PBUF_POOL_BUFSIZE 1540 // enough for 1500 MTU + headers (14 + 22 + 4) +#define PBUF_LINK_HLEN 14 +#define PBUF_LINK_ENCAPSULATION_HLEN 22 // CYW43 WiFi header space +// #define LWIP_NETIF_TX_SINGLE_PBUF 1 // reject chained TX (if available) + +#define MEMP_NUM_PBUF 32 +#define MEMP_NUM_RAW_PCB 32 +#define MEMP_NUM_TCP_PCB 8 +#define MEMP_NUM_TCP_SEG 32 +#define MEMP_NUM_UDP_PCB 8 +#define MEMP_NUM_SYS_TIMEOUT 16 + +// callbacks +#define LWIP_NETIF_LINK_CALLBACK 1 +#define LWIP_NETIF_STATUS_CALLBACK 1 +#define LWIP_NETIF_EXT_STATUS_CALLBACK 1 + +// stats +#define LWIP_STATS 1 +#define LINK_STATS 1 +#define IP_STATS 1 +#define ICMP_STATS 1 +#define IGMP_STATS 1 +#define IPFRAG_STATS 1 +#define UDP_STATS 1 +#define TCP_STATS 1 +#define MEM_STATS 1 +#define MEMP_STATS 1 +#define PBUF_STATS 1 +#define SYS_STATS 1 + +#endif // lwipopts_h diff --git a/examples/raspberrypi/rp2xxx/src/net/lwip/net.zig b/examples/raspberrypi/rp2xxx/src/net/lwip/net.zig new file mode 100644 index 000000000..8ce89eff5 --- /dev/null +++ b/examples/raspberrypi/rp2xxx/src/net/lwip/net.zig @@ -0,0 +1,310 @@ +/// Platform independent network library. Connects lwip with underlying network +/// link interface. +const std = @import("std"); + +const log = std.log.scoped(.lwip); +const assert = std.debug.assert; + +const c = @cImport({ + @cInclude("lwip/init.h"); + @cInclude("lwip/tcpip.h"); + @cInclude("lwip/netif.h"); + @cInclude("lwip/dhcp.h"); + @cInclude("lwip/tcp.h"); + @cInclude("lwip/udp.h"); + @cInclude("lwip/etharp.h"); + @cInclude("lwip/ethip6.h"); + @cInclude("lwip/timeouts.h"); +}); + +const Self = @This(); + +netif: c.netif = .{}, +dhcp: c.dhcp = .{}, + +mac: [6]u8, +link: struct { + ptr: *anyopaque, + recv: *const fn (*anyopaque, []u8) anyerror!?struct { usize, usize }, + send: *const fn (*anyopaque, []u8) anyerror!void, + ready: *const fn (*anyopaque) bool, +}, + +pub fn init(self: *Self) !void { + c.lwip_init(); + const netif: *c.netif = &self.netif; + + _ = c.netif_add( + netif, + @as(*c.ip4_addr_t, @ptrCast(@constCast(c.IP4_ADDR_ANY))), // ipaddr + @as(*c.ip4_addr_t, @ptrCast(@constCast(c.IP4_ADDR_ANY))), // netmask + @as(*c.ip4_addr_t, @ptrCast(@constCast(c.IP4_ADDR_ANY))), // gw + null, + netif_init, + c.netif_input, + ) orelse return error.OutOfMemory; + + c.netif_create_ip6_linklocal_address(netif, 1); + netif.ip6_autoconfig_enabled = 1; + c.netif_set_status_callback(netif, netif_status_callback); + c.netif_set_default(netif); + + c.dhcp_set_struct(netif, &self.dhcp); + c.netif_set_up(netif); + try c_err(c.dhcp_start(netif)); + c.netif_set_link_up(netif); +} + +fn netif_init(netif_c: [*c]c.netif) callconv(.c) c.err_t { + const netif: *c.netif = netif_c; + const self: *Self = @fieldParentPtr("netif", netif); + + netif.linkoutput = netif_linkoutput; + netif.output = c.etharp_output; + netif.output_ip6 = c.ethip6_output; + netif.mtu = sz.mtu; + netif.flags = c.NETIF_FLAG_BROADCAST | c.NETIF_FLAG_ETHARP | c.NETIF_FLAG_ETHERNET | c.NETIF_FLAG_IGMP | c.NETIF_FLAG_MLD6; + std.mem.copyForwards(u8, &netif.hwaddr, &self.mac); + netif.hwaddr_len = c.ETH_HWADDR_LEN; + return c.ERR_OK; +} + +fn netif_status_callback(netif_c: [*c]c.netif) callconv(.c) void { + const netif: *c.netif = netif_c; + const self: *Self = @fieldParentPtr("netif", netif); + log.debug("netif status callback is_link_up: {}, is_up: {}, ready: {}, ip: {f}", .{ + netif.flags & c.NETIF_FLAG_LINK_UP > 0, + netif.flags & c.NETIF_FLAG_UP > 0, + self.ready(), + IPFormatter.new(netif.ip_addr), + }); +} + +/// Called by lwip when there is packet to send. +/// pbuf chain total_len is <= netif.mtu + ethernet header +fn netif_linkoutput(netif_c: [*c]c.netif, pbuf_c: [*c]c.pbuf) callconv(.c) c.err_t { + const netif: *c.netif = netif_c; + var pbuf: *c.pbuf = pbuf_c; + const self: *Self = @fieldParentPtr("netif", netif); + + if (!self.link.ready(self.link.ptr)) { + log.err("linkouput link not ready", .{}); + return c.ERR_MEM; // lwip will try later + } + + if (c.pbuf_header(pbuf, sz.link_head) != 0) { + log.err("can't get pbuf headroom len: {}, tot_len: {} ", .{ pbuf.len, pbuf.tot_len }); + return c.ERR_ARG; + } + + if (pbuf.next != null) { + // clone chain into single packet buffer + pbuf = c.pbuf_clone(c.PBUF_RAW, c.PBUF_POOL, pbuf) orelse return c.ERR_MEM; + } + defer { + // free local clone is clone was made + if (pbuf_c.*.next != null) _ = c.pbuf_free(pbuf); + } + + self.link.send(self.link.ptr, payload_bytes(pbuf)) catch |err| { + log.err("link send {}", .{err}); + return c.ERR_ARG; + }; + return c.ERR_OK; +} + +fn payload_bytes(pbuf: *c.pbuf) []u8 { + return @as([*]u8, @ptrCast(pbuf.payload.?))[0..pbuf.len]; +} + +pub fn ready(self: *Self) bool { + const netif = &self.netif; + return (netif.flags & c.NETIF_FLAG_UP > 0) and + (netif.flags & c.NETIF_FLAG_LINK_UP > 0) and + (netif.ip_addr.u_addr.ip4.addr != 0 or netif.ip_addr.u_addr.ip6.addr[0] != 0); +} + +pub fn poll(self: *Self) !void { + var mem_err_count: usize = 0; + while (true) { + // get packet buffer of the max size + const pbuf: *c.pbuf = c.pbuf_alloc(c.PBUF_RAW, sz.pbuf_pool, c.PBUF_POOL) orelse { + if (mem_err_count > 2) { + self.log_stats(); + return error.OutOfMemory; + } + mem_err_count += 1; + c.sys_check_timeouts(); + continue; + }; + mem_err_count = 0; + assert(pbuf.next == null); + assert(pbuf.len == pbuf.tot_len and pbuf.len == sz.pbuf_pool); + + // receive into that buffer + const head, const len = try self.link.recv(self.link.ptr, payload_bytes(pbuf)) orelse { + // no data release packet buffer and exit loop + _ = c.pbuf_free(pbuf); + break; + }; + errdefer _ = c.pbuf_free(pbuf); // netif.input: transfers ownership of pbuf on success + // set payload header and len + if (c.pbuf_header(pbuf, -@as(c.s16_t, @intCast(head))) != 0) return error.InvalidPbufHead; + pbuf.len = @intCast(len); + pbuf.tot_len = @intCast(len); + // pass data to the lwip input function + try c_err(self.netif.input.?(pbuf, &self.netif)); + } + c.sys_check_timeouts(); +} + +pub fn log_stats(self: *Self) void { + _ = self; + const stats = c.lwip_stats; + log.debug("stats ip_frag: {}", .{stats.ip_frag}); + log.debug("stats icpmp: {}", .{stats.icmp}); + log.debug("stats mem: {} ", .{stats.mem}); + for (stats.memp, 0..) |s, i| { + log.debug("stats memp {}: {}", .{ i, s.* }); + } +} + +pub const Udp = struct { + pcb: c.udp_pcb = .{}, + addr: c.ip_addr_t = .{}, + port: u16 = 0, + + fn init(udp: *Udp, net: *Self, target: []const u8, port: u16) !void { + if (c.ipaddr_aton(target.ptr, &udp.addr) != 1) return error.IpAddrParse; + c.udp_bind_netif(&udp.pcb, &net.netif); + udp.port = port; + udp.pcb.ttl = 64; + } + + pub fn send(udp: *Udp, data: []const u8) !void { + const pbuf: *c.pbuf = c.pbuf_alloc(c.PBUF_TRANSPORT, @intCast(data.len), c.PBUF_POOL) orelse return error.OutOfMemory; + defer _ = c.pbuf_free(pbuf); + try c_err(c.pbuf_take(pbuf, data.ptr, @intCast(data.len))); + try c_err(c.udp_sendto(&udp.pcb, pbuf, &udp.addr, udp.port)); + } +}; + +pub fn udp_init(self: *Self, udp: *Udp, target: []const u8, port: u16) !void { + try udp.init(self, target, port); +} + +const Error = error{ + /// Out of memory error. + OutOfMemory, + /// Buffer error. + BufferError, + /// Timeout. + Timeout, + /// Routing problem. + Routing, + /// Operation in progress + InProgress, + /// Illegal value. + IllegalValue, + /// Operation would block. + WouldBlock, + /// Address in use. + AddressInUse, + /// Already connecting. + AlreadyConnecting, + /// Conn already established. + AlreadyConnected, + /// Not connected. + NotConnected, + /// Low-level netif error + LowlevelInterfaceError, + /// Connection aborted. + ConnectionAborted, + /// Connection reset. + ConnectionReset, + /// Connection closed. + ConnectionClosed, + /// Illegal argument. + IllegalArgument, + /// + Unknown, +}; + +fn c_err(res: anytype) Error!void { + switch (@TypeOf(res)) { + c.err_t => return switch (res) { + c.ERR_OK => {}, + c.ERR_MEM => Error.OutOfMemory, + c.ERR_BUF => Error.BufferError, + c.ERR_TIMEOUT => Error.Timeout, + c.ERR_RTE => Error.Routing, + c.ERR_INPROGRESS => Error.InProgress, + c.ERR_VAL => Error.IllegalValue, + c.ERR_WOULDBLOCK => Error.WouldBlock, + c.ERR_USE => Error.AddressInUse, + c.ERR_ALREADY => Error.AlreadyConnecting, + c.ERR_ISCONN => Error.AlreadyConnected, + c.ERR_CONN => Error.NotConnected, + c.ERR_IF => Error.LowlevelInterfaceError, + c.ERR_ABRT => Error.ConnectionAborted, + c.ERR_RST => Error.ConnectionReset, + c.ERR_CLSD => Error.ConnectionClosed, + c.ERR_ARG => Error.IllegalArgument, + else => { + log.err("error code: {}", .{res}); + @panic("unexpected lwip error code!"); + }, + }, + c.u8_t => { + if (res != 0) { + return Error.Unknown; + } + }, + else => @compileError("unknown type"), + } +} + +const IPFormatter = struct { + addr: c.ip_addr_t, + + pub fn new(addr: c.ip_addr_t) IPFormatter { + return IPFormatter{ .addr = addr }; + } + + pub fn format( + addr: IPFormatter, + writer: anytype, + ) !void { + try writer.writeAll(std.mem.sliceTo(c.ip4addr_ntoa(@as(*const c.ip4_addr_t, @ptrCast(&addr.addr))), 0)); + } +}; + +// required buffer sizes +const sz = struct { + const pbuf_pool = c.PBUF_POOL_BUFSIZE; // 1540 = 1500 mtu + ethernet + link head/tail + const link_head = c.PBUF_LINK_ENCAPSULATION_HLEN; // 22 + const link_tail = 4; // reserved for the status in recv buffer + const ethernet = 14; // layer 2 ethernet header size + + // layer 3 (ip) mtu, + const mtu = pbuf_pool - link_head - link_tail - ethernet; // 1500 + + // ip v4 sizes + const v4 = struct { + // headers + const ip = 20; + const udp = 8; + const tcp = 20; + + const payload = struct { + const udp = mtu - ip - v4.udp; // 1472 + }; + }; +}; + +// test lwipopts.h config +comptime { + assert(sz.pbuf_pool == 1540); + assert(sz.link_head == 22); + assert(sz.mtu == 1500); +} diff --git a/examples/raspberrypi/rp2xxx/src/net/lwip/readme.md b/examples/raspberrypi/rp2xxx/src/net/lwip/readme.md new file mode 100644 index 000000000..4cedaa571 --- /dev/null +++ b/examples/raspberrypi/rp2xxx/src/net/lwip/readme.md @@ -0,0 +1,4 @@ + +net.zig - Chip independent network library. Connects lwip with underlying network link interface. +exports.zig - Platform dependent exports required by lwip. +include - lwip configuration files diff --git a/examples/raspberrypi/rp2xxx/src/net/secrets.zig b/examples/raspberrypi/rp2xxx/src/net/secrets.zig new file mode 100644 index 000000000..5689acaa4 --- /dev/null +++ b/examples/raspberrypi/rp2xxx/src/net/secrets.zig @@ -0,0 +1,2 @@ +pub const ssid = "..."; +pub const pwd = "..."; diff --git a/modules/lwip/build.zig b/modules/lwip/build.zig new file mode 100644 index 000000000..e53a1faa2 --- /dev/null +++ b/modules/lwip/build.zig @@ -0,0 +1,127 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const upstream = b.dependency("lwip", .{}); + + const lwip = b.addModule("lwip", .{ + .target = target, + .optimize = optimize, + }); + + lwip.addCSourceFiles(.{ + .root = upstream.path("src"), + .files = &files, + .flags = &flags, + }); + lwip.addIncludePath(upstream.path("src/include")); +} + +// pub fn setup(b: *std.Build, dst: *std.Build.Module) void { +// const upstream = b.dependency("lwip", .{}); +// dst.addIncludePath(upstream.path("src/include")); +// dst.addIncludePath(b.path("src/kernel/components/network/include")); +// } + +const flags = [_][]const u8{ "-std=c99", "-fno-sanitize=undefined" }; +const files = [_][]const u8{ + // Core files + "core/init.c", + "core/udp.c", + "core/inet_chksum.c", + "core/altcp_alloc.c", + "core/stats.c", + "core/altcp.c", + "core/mem.c", + "core/ip.c", + "core/pbuf.c", + "core/netif.c", + "core/tcp_out.c", + "core/dns.c", + "core/tcp_in.c", + "core/memp.c", + "core/tcp.c", + "core/sys.c", + "core/def.c", + "core/timeouts.c", + "core/raw.c", + "core/altcp_tcp.c", + + // IPv4 implementation: + "core/ipv4/dhcp.c", + "core/ipv4/autoip.c", + "core/ipv4/ip4_frag.c", + "core/ipv4/etharp.c", + "core/ipv4/ip4.c", + "core/ipv4/ip4_addr.c", + "core/ipv4/igmp.c", + "core/ipv4/icmp.c", + + // IPv6 implementation: + "core/ipv6/icmp6.c", + "core/ipv6/ip6_addr.c", + "core/ipv6/ip6.c", + "core/ipv6/ip6_frag.c", + "core/ipv6/mld6.c", + "core/ipv6/dhcp6.c", + "core/ipv6/inet6.c", + "core/ipv6/ethip6.c", + "core/ipv6/nd6.c", + + // Interfaces: + "netif/bridgeif.c", + "netif/ethernet.c", + // "netif/slipif.c", + "netif/bridgeif_fdb.c", + + // sequential APIs + // "api/err.c", + // "api/api_msg.c", + // "api/netifapi.c", + // "api/sockets.c", + // "api/netbuf.c", + // "api/api_lib.c", + // "api/tcpip.c", + // "api/netdb.c", + // "api/if_api.c", + + // 6LoWPAN + "netif/lowpan6.c", + "netif/lowpan6_ble.c", + "netif/lowpan6_common.c", + "netif/zepif.c", + + // PPP + // "netif/ppp/polarssl/arc4.c", + // "netif/ppp/polarssl/des.c", + // "netif/ppp/polarssl/md4.c", + // "netif/ppp/polarssl/sha1.c", + // "netif/ppp/polarssl/md5.c", + // "netif/ppp/ipcp.c", + // "netif/ppp/magic.c", + // "netif/ppp/pppoe.c", + // "netif/ppp/mppe.c", + // "netif/ppp/multilink.c", + // "netif/ppp/chap-new.c", + // "netif/ppp/auth.c", + // "netif/ppp/chap_ms.c", + // "netif/ppp/ipv6cp.c", + // "netif/ppp/chap-md5.c", + // "netif/ppp/upap.c", + // "netif/ppp/pppapi.c", + // "netif/ppp/pppos.c", + // "netif/ppp/eap.c", + // "netif/ppp/pppol2tp.c", + // "netif/ppp/demand.c", + // "netif/ppp/fsm.c", + // "netif/ppp/eui64.c", + // "netif/ppp/ccp.c", + // "netif/ppp/pppcrypt.c", + // "netif/ppp/utils.c", + // "netif/ppp/vj.c", + // "netif/ppp/lcp.c", + // "netif/ppp/ppp.c", + // "netif/ppp/ecp.c", +}; diff --git a/modules/lwip/build.zig.zon b/modules/lwip/build.zig.zon new file mode 100644 index 000000000..692c9058c --- /dev/null +++ b/modules/lwip/build.zig.zon @@ -0,0 +1,15 @@ +.{ + .name = "lwip", + .version = "0.1.0", + .paths = .{ + "build.zig", + "build.zig.zon", + }, + .dependencies = .{ + .lwip = .{ + //.url = "https://download.savannah.nongnu.org/releases/lwip/lwip-2.1.3.zip", + .url = "https://github.com/Ashet-Technologies/deps/raw/refs/heads/main/lwip-2.1.3.zip", + .hash = "N-V-__8AAKCP6ADEah-rtpC4uEsB_-GFFCqsNInUAJY7bH3S", + }, + }, +} diff --git a/port/raspberrypi/rp2xxx/src/hal/cyw43439_pio_spi.zig b/port/raspberrypi/rp2xxx/src/hal/cyw43439_pio_spi.zig new file mode 100644 index 000000000..f3e99a47f --- /dev/null +++ b/port/raspberrypi/rp2xxx/src/hal/cyw43439_pio_spi.zig @@ -0,0 +1,189 @@ +//! CYW43 3-wire SPI driver build on PIO +//! Code based on embassy cyw43-pio driver https://github.com/embassy-rs/embassy/tree/main/cyw43-pio (last commit: d41eeea) +const std = @import("std"); +const mem = std.mem; +const microzig = @import("microzig"); +const hal = @import("../hal.zig"); + +const cyw43spi_program = blk: { + @setEvalBranchQuota(5000); + break :blk hal.pio.assemble( + \\.program cyw43spi + \\.side_set 1 + \\ + \\.wrap_target + \\ + \\; write out x-1 bits + \\lp: + \\out pins, 1 side 0 + \\jmp x-- lp side 1 + \\ + \\; switch directions + \\set pindirs, 0 side 0 + \\nop side 0 + \\ + \\; read in y-1 bits + \\lp2: + \\in pins, 1 side 1 + \\jmp y-- lp2 side 0 + \\ + \\; wait for event and irq host + \\wait 1 pin 0 side 0 + \\irq 0 side 0 + \\ + \\.wrap + , .{}).get_program_by_name("cyw43spi"); +}; + +fn pin_num(pin: hal.gpio.Pin) u5 { + return @truncate(@intFromEnum(pin)); +} + +const Self = @This(); + +pio: hal.pio.Pio, +sm: hal.pio.StateMachine, +channel: ?hal.dma.Channel = null, // current dma channel +pins: Pins, + +pub const Config = struct { + pio: hal.pio.Pio = hal.pio.num(0), + pins: Pins = .{}, +}; + +pub const Pins = struct { + cs: hal.gpio.Pin = hal.gpio.num(25), + io: hal.gpio.Pin = hal.gpio.num(24), + clk: hal.gpio.Pin = hal.gpio.num(29), + pwr: hal.gpio.Pin = hal.gpio.num(23), +}; + +pub fn init(config: Config) !Self { + const pins = config.pins; + const pio = config.pio; + const sm = try pio.claim_unused_state_machine(); + + // Chip select pin setup + pins.cs.set_function(.sio); + pins.cs.set_direction(.out); + pins.cs.put(1); + + // IO pin setup + pio.gpio_init(pins.io); + pins.io.set_output_disabled(false); + pins.io.set_pull(.disabled); + pins.io.set_schmitt_trigger_enabled(true); + try pio.set_input_sync_bypass(pins.io); + pins.io.set_drive_strength(.@"12mA"); + pins.io.set_slew_rate(.fast); + + // Clock pin setup + pio.gpio_init(pins.clk); + pins.clk.set_output_disabled(false); + pins.clk.set_drive_strength(.@"12mA"); + pins.clk.set_slew_rate(.fast); + + try pio.sm_load_and_start_program(sm, cyw43spi_program, .{ + .clkdiv = .{ + // 50MHz is recomended by datasheet + .int = if (hal.compatibility.chip == .RP2040) 2 else 3, + .frac = 0, + }, + .pin_mappings = .{ + .out = .single(pins.io), + .set = .single(pins.io), + .side_set = .single(pins.clk), + .in_base = pins.io, + }, + .shift = .{ + .out_shiftdir = .left, + .in_shiftdir = .left, + .autopull = true, + .autopush = true, + }, + }); + + try pio.sm_set_pindir(sm, pins.clk, 1, .out); + try pio.sm_set_pindir(sm, pins.io, 1, .out); + try pio.sm_set_pin(sm, pins.clk, 1, 0); + try pio.sm_set_pin(sm, pins.io, 1, 0); + + // Power init sequence + pins.pwr.set_function(.sio); + pins.pwr.set_direction(.out); + pins.pwr.put(0); + hal.time.sleep_ms(50); + pins.pwr.put(1); + hal.time.sleep_ms(250); + + return .{ + .pio = pio, + .sm = sm, + .pins = pins, + }; +} + +pub fn read(ptr: *anyopaque, words: []u32) void { + const self: *Self = @ptrCast(@alignCast(ptr)); + self.pins.cs.put(0); + defer self.pins.cs.put(1); + + self.channel = hal.dma.claim_unused_channel(); + defer self.channel.?.unclaim(); + + self.prep(words.len * 32 - 1, 31); + self.dma_write(words[0..1]); + self.dma_read(words); // last word is status +} + +// By default it sends status after each read/write. +// ref: datasheet 'Table 6. gSPI Registers' status enable has default 1 +pub fn write(ptr: *anyopaque, words: []u32) void { + const self: *Self = @ptrCast(@alignCast(ptr)); + self.pins.cs.put(0); + defer self.pins.cs.put(1); + + self.channel = hal.dma.claim_unused_channel(); + defer self.channel.?.unclaim(); + + self.prep(31, words.len * 32 - 1); + self.dma_write(words); + self.dma_read(words[0..1]); // read status into first word +} + +fn prep(self: *Self, read_bits: u32, write_bits: u32) void { + self.pio.sm_set_enabled(self.sm, false); + self.pio.sm_exec_set_y(self.sm, read_bits); + self.pio.sm_exec_set_x(self.sm, write_bits); + self.pio.sm_exec_set_pindir(self.sm, 0b1); + self.pio.sm_exec_jmp(self.sm, cyw43spi_program.wrap_target.?); + self.pio.sm_set_enabled(self.sm, true); +} + +fn dma_read(self: *Self, data: []u32) void { + const ch = self.channel.?; + const rx_fifo_addr = @intFromPtr(self.pio.sm_get_rx_fifo(self.sm)); + ch.setup_transfer_raw(@intFromPtr(data.ptr), rx_fifo_addr, data.len, .{ + .trigger = true, + .data_size = .size_32, + .enable = true, + .read_increment = false, + .write_increment = true, + .dreq = @enumFromInt(@intFromEnum(self.pio) * @as(u6, 8) + @intFromEnum(self.sm) + 4), + }); + ch.wait_for_finish_blocking(); +} + +fn dma_write(self: *Self, data: []const u32) void { + const ch = self.channel.?; + const tx_fifo_addr = @intFromPtr(self.pio.sm_get_tx_fifo(self.sm)); + ch.setup_transfer_raw(tx_fifo_addr, @intFromPtr(data.ptr), data.len, .{ + .trigger = true, + .data_size = .size_32, + .enable = true, + .read_increment = true, + .write_increment = false, + .dreq = @enumFromInt(@intFromEnum(self.pio) * @as(u6, 8) + @intFromEnum(self.sm)), + }); + ch.wait_for_finish_blocking(); +} diff --git a/port/raspberrypi/rp2xxx/src/hal/drivers.zig b/port/raspberrypi/rp2xxx/src/hal/drivers.zig index f9a2cc6bc..913096b77 100644 --- a/port/raspberrypi/rp2xxx/src/hal/drivers.zig +++ b/port/raspberrypi/rp2xxx/src/hal/drivers.zig @@ -506,3 +506,32 @@ pub const CYW43_Pio_Device = struct { } } }; + +pub const WiFi = struct { + const Self = @This(); + pub const Chip = mdf.wireless.Cyw43439; + pub const recv = Chip.recv_zc; + pub const send = Chip.send_zc; + pub const ready = Chip.ready; + + const Spi = @import("cyw43439_pio_spi.zig"); + pub const Config = Spi.Config; + + spi: Spi = undefined, + chip: Chip = .{}, // cyw43 chip interface + + pub fn init(self: *Self, config: Config) !*Chip { + self.spi = try Spi.init(config); + try self.chip.init( + .{ + .ptr = &self.spi, + .vtable = &.{ + .read = Spi.read, + .write = Spi.write, + }, + }, + hal.time.sleep_ms, + ); + return &self.chip; + } +};