diff --git a/core/src/interrupt.zig b/core/src/interrupt.zig index 09fffacd8..acec6de10 100644 --- a/core/src/interrupt.zig +++ b/core/src/interrupt.zig @@ -122,9 +122,10 @@ pub const CriticalSectionMutex = struct { /// Unlocks the mutex. pub fn unlock(self: *CriticalSectionMutex) void { - if (self.critical_section) |cs| { + const maybe_cs = self.critical_section; + self.critical_section = null; + if (maybe_cs) |cs| { cs.leave(); - self.critical_section = null; } } }; diff --git a/examples/espressif/esp/build.zig b/examples/espressif/esp/build.zig index 1029174cc..f902ca26a 100644 --- a/examples/espressif/esp/build.zig +++ b/examples/espressif/esp/build.zig @@ -12,13 +12,7 @@ pub fn build(b: *std.Build) void { const mz_dep = b.dependency("microzig", .{}); const mb = MicroBuild.init(b, mz_dep) orelse return; - const targets = [_]TargetDescription{ - .{ .prefix = "esp32_c3", .target = mb.ports.esp.chips.esp32_c3 }, - .{ .prefix = "esp32_c3_direct_boot", .target = mb.ports.esp.chips.esp32_c3_direct_boot }, - .{ .prefix = "esp32_c3_flashless", .target = mb.ports.esp.chips.esp32_c3_flashless }, - }; - - const available_examples = [_]Example{ + const examples: []const Example = &.{ .{ .name = "blinky", .file = "src/blinky.zig" }, .{ .name = "custom_clock_config", .file = "src/custom_clock_config.zig" }, .{ .name = "gpio_input", .file = "src/gpio_input.zig" }, @@ -30,15 +24,25 @@ pub fn build(b: *std.Build) void { .{ .name = "stepper_driver_dumb", .file = "src/stepper_driver_dumb.zig" }, .{ .name = "systimer", .file = "src/systimer.zig" }, .{ .name = "ws2812_blinky", .file = "src/ws2812_blinky.zig" }, + .{ .name = "rtos", .file = "src/rtos.zig" }, + .{ .name = "tcp_server", .file = "src/tcp_server.zig", .features = .{ + .flashless = false, + .lwip = true, + } }, }; - for (available_examples) |example| { + for (examples) |example| { // If we specify example, only select the ones that match if (maybe_example) |selected_example| if (!std.mem.containsAtLeast(u8, example.name, 1, selected_example)) continue; - for (targets) |target_desc| { + for (std.enums.values(TargetEnum)) |target_enum| { + if (!example.features.flashless and std.mem.containsAtLeast(u8, @tagName(target_enum), 1, "flashless")) + continue; + + const target_desc = target_enum.get_target_desc(mb); + // `add_firmware` basically works like addExecutable, but takes a // `microzig.Target` for target instead of a `std.zig.CrossTarget`. // @@ -51,6 +55,28 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path(example.file), }); + if (example.features.lwip) { + const target = b.resolveTargetQuery(firmware.target.zig_target); + + const foundation_dep = b.dependency("foundation_libc", .{ + .target = target, + .optimize = optimize, + .single_threaded = true, + }); + + const lwip_dep = b.dependency("lwip", .{ + .target = target, + .optimize = optimize, + .include_dir = b.path("src/lwip/include"), + }); + + const libc_lib = foundation_dep.artifact("foundation"); + const lwip_lib = lwip_dep.artifact("lwip"); + + lwip_lib.root_module.linkLibrary(libc_lib); + firmware.app_mod.linkLibrary(lwip_lib); + } + // `installFirmware()` is the MicroZig pendant to `Build.installArtifact()` // and allows installing the firmware as a typical firmware file. // @@ -63,12 +89,146 @@ pub fn build(b: *std.Build) void { } } +const TargetEnum = enum { + esp32_c3, + esp32_c3_direct_boot, + esp32_c3_flashless, + + fn get_target_desc(target_enum: TargetEnum, mb: *MicroBuild) TargetDescription { + return switch (target_enum) { + .esp32_c3 => .{ + .prefix = "esp32_c3", + .target = mb.ports.esp.chips.esp32_c3, + }, + .esp32_c3_direct_boot => .{ + .prefix = "esp32_c3_direct_boot", + .target = mb.ports.esp.chips.esp32_c3_direct_boot, + }, + .esp32_c3_flashless => .{ + .prefix = "esp32_c3_flashless", + .target = mb.ports.esp.chips.esp32_c3_flashless, + }, + }; + } +}; + const TargetDescription = struct { prefix: []const u8, target: *const microzig.Target, }; const Example = struct { + const Features = packed struct { + flashless: bool = true, + lwip: bool = false, + }; + name: []const u8, file: []const u8, + features: Features = .{}, +}; + +const lwip_flags = [_][]const u8{ "-std=c99", "-fno-sanitize=undefined" }; +const lwip_files = [_][]const u8{ + // Core files + "core/init.c", + "core/def.c", + "core/dns.c", + "core/inet_chksum.c", + "core/ip.c", + "core/mem.c", + "core/memp.c", + "core/netif.c", + "core/pbuf.c", + "core/raw.c", + "core/stats.c", + "core/sys.c", + "core/altcp.c", + "core/altcp_alloc.c", + "core/altcp_tcp.c", + "core/tcp.c", + "core/tcp_in.c", + "core/tcp_out.c", + "core/timeouts.c", + "core/udp.c", + + // IPv4 implementation: + "core/ipv4/acd.c", + "core/ipv4/autoip.c", + "core/ipv4/dhcp.c", + "core/ipv4/etharp.c", + "core/ipv4/icmp.c", + "core/ipv4/igmp.c", + "core/ipv4/ip4_frag.c", + "core/ipv4/ip4.c", + "core/ipv4/ip4_addr.c", + + // IPv6 implementation: + "core/ipv6/dhcp6.c", + "core/ipv6/ethip6.c", + "core/ipv6/icmp6.c", + "core/ipv6/inet6.c", + "core/ipv6/ip6.c", + "core/ipv6/ip6_addr.c", + "core/ipv6/ip6_frag.c", + "core/ipv6/mld6.c", + "core/ipv6/nd6.c", + + // Interfaces + "netif/ethernet.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/examples/espressif/esp/build.zig.zon b/examples/espressif/esp/build.zig.zon index 83c09a474..3b85b142c 100644 --- a/examples/espressif/esp/build.zig.zon +++ b/examples/espressif/esp/build.zig.zon @@ -4,6 +4,8 @@ .version = "0.0.0", .dependencies = .{ .microzig = .{ .path = "../../.." }, + .lwip = .{ .path = "../../../modules/lwip" }, + .foundation_libc = .{ .path = "../../../modules/foundation-libc" }, }, .paths = .{ "README.md", diff --git a/examples/espressif/esp/src/lwip/exports.zig b/examples/espressif/esp/src/lwip/exports.zig new file mode 100644 index 000000000..93f38dffe --- /dev/null +++ b/examples/espressif/esp/src/lwip/exports.zig @@ -0,0 +1,200 @@ +/// Platform dependent exports required by lwip +/// +const std = @import("std"); +const assert = std.debug.assert; + +const microzig = @import("microzig"); +const esp = microzig.hal; +const rtos = esp.rtos; + +const c = @import("include.zig").c; + +const log = std.log.scoped(.lwip); + +// TODO: mutex and mailbox can be implemented without allocations if we define +// the correct types in sys_arch.h + +pub var gpa: std.mem.Allocator = undefined; +pub var core_mutex: rtos.Mutex = .{}; + +export fn lwip_lock_core_mutex() void { + core_mutex.lock(); +} + +export fn lwip_unlock_core_mutex() void { + core_mutex.unlock(); +} + +export fn lwip_rand() u32 { + return esp.rng.random_u32(); +} + +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 }); +} + +export fn sys_now() u32 { + const ts = esp.time.get_time_since_boot(); + return @truncate(ts.to_us() / 1000); +} + +export fn sys_init() void {} + +export fn sys_mutex_new(ptr: *c.sys_mutex_t) c.err_t { + const mutex = gpa.create(rtos.Mutex) catch { + log.warn("failed to allocate mutex", .{}); + return c.ERR_MEM; + }; + mutex.* = .{}; + ptr.* = mutex; + return c.ERR_OK; +} + +export fn sys_mutex_free(ptr: *c.sys_mutex_t) void { + const mutex: *rtos.Mutex = @ptrCast(@alignCast(ptr.*)); + gpa.destroy(mutex); +} + +export fn sys_mutex_lock(ptr: *c.sys_mutex_t) void { + const mutex: *rtos.Mutex = @ptrCast(@alignCast(ptr.*)); + mutex.lock(); +} + +export fn sys_mutex_unlock(ptr: *c.sys_mutex_t) void { + const mutex: *rtos.Mutex = @ptrCast(@alignCast(ptr.*)); + mutex.unlock(); +} + +const MailboxElement = ?*anyopaque; +const Mailbox = rtos.Queue(MailboxElement); + +export fn sys_mbox_new(ptr: *c.sys_mbox_t, size: i32) c.err_t { + const buffer = gpa.alloc(MailboxElement, @intCast(size)) catch { + log.warn("failed to allocate mailbox buffer", .{}); + return c.ERR_MEM; + }; + + const mailbox = gpa.create(Mailbox) catch { + log.warn("failed to allocate mailbox", .{}); + gpa.free(buffer); + return c.ERR_MEM; + }; + mailbox.* = .init(buffer); + + ptr.* = mailbox; + return c.ERR_OK; +} + +export fn sys_mbox_free(ptr: *c.sys_mbox_t) void { + const mailbox: *Mailbox = @ptrCast(@alignCast(ptr.*)); + gpa.free(@as([]MailboxElement, @ptrCast(@alignCast(mailbox.type_erased.buffer)))); + gpa.destroy(mailbox); + ptr.* = null; +} + +export fn sys_mbox_valid(ptr: *c.sys_mbox_t) c_int { + return @intFromBool(ptr.* != null); +} + +export fn sys_mbox_set_invalid(ptr: *c.sys_mbox_t) void { + ptr.* = null; +} + +export fn sys_mbox_post(ptr: *c.sys_mbox_t, element: MailboxElement) void { + const mailbox: *Mailbox = @ptrCast(@alignCast(ptr.*)); + mailbox.put_one(element, null) catch unreachable; +} + +export fn sys_mbox_trypost(ptr: *c.sys_mbox_t, element: MailboxElement) c.err_t { + const mailbox: *Mailbox = @ptrCast(@alignCast(ptr.*)); + if (mailbox.put_one_non_blocking(element)) { + return c.ERR_OK; + } else { + return c.ERR_MEM; + } +} + +comptime { + @export(&sys_mbox_trypost, .{ .name = "sys_mbox_trypost_fromisr" }); +} + +export fn sys_arch_mbox_fetch(ptr: *c.sys_mbox_t, element_ptr: *MailboxElement, timeout: u32) u32 { + const mailbox: *Mailbox = @ptrCast(@alignCast(ptr.*)); + const now = esp.time.get_time_since_boot(); + element_ptr.* = mailbox.get_one(if (timeout != 0) .from_ms(timeout) else null) catch { + return c.SYS_ARCH_TIMEOUT; + }; + // returns waiting time in ms + return @intCast(esp.time.get_time_since_boot().diff(now).to_us() / 1_000); +} + +export fn sys_arch_mbox_tryfetch(ptr: *c.sys_mbox_t, element_ptr: *MailboxElement) u32 { + const mailbox: *Mailbox = @ptrCast(@alignCast(ptr.*)); + if (mailbox.get_one_non_blocking()) |element| { + element_ptr.* = element; + return 0; + } else { + return c.SYS_MBOX_EMPTY; + } +} + +export fn sys_sem_new(ptr: *c.sys_sem_t, count: u8) c.err_t { + const sem = gpa.create(rtos.Semaphore) catch { + log.warn("failed to allocate semaphore", .{}); + return c.ERR_MEM; + }; + sem.* = .init(count, 1); + ptr.* = sem; + return c.ERR_OK; +} + +export fn sys_sem_free(ptr: *c.sys_sem_t) void { + const sem: *rtos.Semaphore = @ptrCast(@alignCast(ptr.*)); + gpa.destroy(sem); +} + +export fn sys_sem_signal(ptr: *c.sys_sem_t) void { + const sem: *rtos.Semaphore = @ptrCast(@alignCast(ptr.*)); + sem.give(); +} + +export fn sys_arch_sem_wait(ptr: *c.sys_sem_t, timeout: u32) u32 { + const sem: *rtos.Semaphore = @ptrCast(@alignCast(ptr.*)); + const now = esp.time.get_time_since_boot(); + sem.take_with_timeout(if (timeout != 0) .from_ms(timeout) else null) catch { + return c.SYS_ARCH_TIMEOUT; + }; + // returns waiting time in ms + return @intCast(esp.time.get_time_since_boot().diff(now).to_us() / 1_000); +} + +export fn sys_sem_valid(ptr: *c.sys_sem_t) c_int { + return @intFromBool(ptr.* != null); +} + +export fn sys_sem_set_invalid(ptr: *c.sys_sem_t) void { + ptr.* = null; +} + +fn task_wrapper( + task_entry: c.lwip_thread_fn, + param: ?*anyopaque, +) void { + task_entry.?(param); +} + +export fn sys_thread_new(name: [*:0]u8, thread: c.lwip_thread_fn, arg: ?*anyopaque, stacksize: c_int, prio: c_int) c.sys_thread_t { + _ = stacksize; + _ = prio; + + return rtos.spawn(gpa, task_wrapper, .{ thread, arg }, .{ + .name = std.mem.span(name), + .stack_size = 4096, + .priority = @enumFromInt(2), + }) catch @panic("failed to allocate lwip task"); +} diff --git a/examples/espressif/esp/src/lwip/include.zig b/examples/espressif/esp/src/lwip/include.zig new file mode 100644 index 000000000..3d7888c7c --- /dev/null +++ b/examples/espressif/esp/src/lwip/include.zig @@ -0,0 +1,70 @@ +pub const c = @cImport({ + @cInclude("lwip/init.h"); + @cInclude("lwip/tcpip.h"); + @cInclude("lwip/netifapi.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"); +}); + +pub fn c_err(res: c.err_t) Error!void { + 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 => @panic("unexpected character"), + }; +} + +pub 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, +}; diff --git a/examples/espressif/esp/src/lwip/include/arch/cc.h b/examples/espressif/esp/src/lwip/include/arch/cc.h new file mode 100644 index 000000000..df5207f4c --- /dev/null +++ b/examples/espressif/esp/src/lwip/include/arch/cc.h @@ -0,0 +1,41 @@ +#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_core_mutex(); +extern void lwip_unlock_core_mutex(); +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 0 + +#define SYS_ARCH_DECL_PROTECT(lev) sys_prot_t lev +#define SYS_ARCH_PROTECT(lev) lwip_lock_core_mutex() +#define SYS_ARCH_UNPROTECT(lev) lwip_unlock_core_mutex() + +#endif // lwip__cc_h diff --git a/examples/espressif/esp/src/lwip/include/arch/sys_arch.h b/examples/espressif/esp/src/lwip/include/arch/sys_arch.h new file mode 100644 index 000000000..106005cab --- /dev/null +++ b/examples/espressif/esp/src/lwip/include/arch/sys_arch.h @@ -0,0 +1,21 @@ +#ifndef __SYS_ARCH_H__ +#define __SYS_ARCH_H__ + +#include + +typedef void* sys_sem_t; +typedef void* sys_mutex_t; +typedef void* sys_mbox_t; +typedef void* sys_thread_t; + +typedef uint32_t sys_prot_t; + +#define SYS_MBOX_NULL NULL +#define SYS_SEM_NULL NULL +#define SYS_MUTEX_NULL NULL + +void* malloc(size_t size); +void* calloc(size_t count, size_t size); +void free(void* ptr); + +#endif /* __SYS_ARCH_H__ */ diff --git a/examples/espressif/esp/src/lwip/include/lwipopts.h b/examples/espressif/esp/src/lwip/include/lwipopts.h new file mode 100644 index 000000000..badf58e86 --- /dev/null +++ b/examples/espressif/esp/src/lwip/include/lwipopts.h @@ -0,0 +1,37 @@ +#ifndef LWIP_LWIPOPTS_H +#define LWIP_LWIPOPTS_H + +#define NO_SYS 0 +#define SYS_LIGHTWEIGHT_PROT 1 + +#define LWIP_TCPIP_CORE_LOCKING 1 +#define LWIP_TCPIP_CORE_LOCKING_INPUT 1 + +#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 + +#define MEM_LIBC_MALLOC 1 +#define MEMP_MEM_MALLOC 1 +#define MEM_ALIGNMENT 4 + +#define TCPIP_THREAD_NAME "lwip_tcpip" +// #define TCPIP_THREAD_STACKSIZE 1024 +// #define TCPIP_THREAD_PRIO 3 + +#define TCPIP_MBOX_SIZE 16 +#define DEFAULT_RAW_RECVMBOX_SIZE 16 +#define DEFAULT_UDP_RECVMBOX_SIZE 16 +#define DEFAULT_TCP_RECVMBOX_SIZE 16 +#define DEFAULT_ACCEPTMBOX_SIZE 16 + +#define LWIP_NETIF_API 1 +#define LWIP_NETIF_STATUS_CALLBACK 1 + +#endif diff --git a/examples/espressif/esp/src/rtos.zig b/examples/espressif/esp/src/rtos.zig new file mode 100644 index 000000000..872673e58 --- /dev/null +++ b/examples/espressif/esp/src/rtos.zig @@ -0,0 +1,56 @@ +const std = @import("std"); +const log = std.log; + +const microzig = @import("microzig"); +const peripherals = microzig.chip.peripherals; +const esp = microzig.hal; +const gpio = esp.gpio; +const systimer = esp.systimer; +const usb_serial_jtag = esp.usb_serial_jtag; +const rtos = esp.rtos; + +pub const microzig_options: microzig.Options = .{ + .logFn = usb_serial_jtag.logger.log, + .interrupts = .{ + .interrupt30 = rtos.general_purpose_interrupt_handler, + .interrupt31 = rtos.yield_interrupt_handler, + }, + .log_level = .debug, + .cpu = .{ + .interrupt_stack = .{ + .enable = true, + .size = 4096, + }, + }, + .hal = .{ + .rtos = .{ + .enable = true, + }, + }, +}; + +var heap_buf: [10 * 1024]u8 = undefined; + +fn task1(queue: *rtos.Queue(u32)) void { + for (0..5) |i| { + queue.put_one(i, null) catch unreachable; + rtos.sleep(.from_ms(500)); + } +} + +pub fn main() !void { + var heap = microzig.Allocator.init_with_buffer(&heap_buf); + const gpa = heap.allocator(); + + var buffer: [1]u32 = undefined; + var queue: rtos.Queue(u32) = .init(&buffer); + + esp.time.sleep_ms(1000); + + _ = try rtos.spawn(gpa, task1, .{&queue}, .{}); + + while (true) { + const item = try queue.get_one(.from_ms(1000)); + std.log.info("got item: {}", .{item}); + } +} diff --git a/examples/espressif/esp/src/systimer.zig b/examples/espressif/esp/src/systimer.zig index 54a028e2f..ce1921f08 100644 --- a/examples/espressif/esp/src/systimer.zig +++ b/examples/espressif/esp/src/systimer.zig @@ -9,7 +9,7 @@ const usb_serial_jtag = hal.usb_serial_jtag; pub const microzig_options: microzig.Options = .{ .logFn = usb_serial_jtag.logger.log, .interrupts = .{ - .interrupt1 = timer_interrupt, + .interrupt1 = .{ .c = timer_interrupt }, }, }; diff --git a/examples/espressif/esp/src/tcp_server.zig b/examples/espressif/esp/src/tcp_server.zig new file mode 100644 index 000000000..8f2729794 --- /dev/null +++ b/examples/espressif/esp/src/tcp_server.zig @@ -0,0 +1,245 @@ +const std = @import("std"); +const assert = std.debug.assert; + +const microzig = @import("microzig"); +const interrupt = microzig.cpu.interrupt; +const hal = microzig.hal; +const rtos = hal.rtos; +const radio = hal.radio; +const usb_serial_jtag = hal.usb_serial_jtag; + +const exports = @import("lwip/exports.zig"); +const lwip = @import("lwip/include.zig"); + +comptime { + _ = exports; +} + +pub const microzig_options: microzig.Options = .{ + .log_level = .debug, + .log_scope_levels = &.{ + .{ .scope = .esp_radio, .level = .err }, + .{ .scope = .esp_radio_timer, .level = .err }, + .{ .scope = .esp_radio_wifi, .level = .err }, + .{ .scope = .esp_radio_osi, .level = .err }, + }, + .logFn = usb_serial_jtag.logger.log, + .interrupts = .{ + .interrupt29 = radio.interrupt_handler, + .interrupt30 = rtos.general_purpose_interrupt_handler, + .interrupt31 = rtos.yield_interrupt_handler, + }, + .cpu = .{ + .interrupt_stack = .{ + .enable = true, + .size = 8192, + }, + }, + .hal = .{ + .rtos = .{ + .enable = true, + }, + .radio = .{ + .wifi = .{ + .on_packet_received = on_packet_received, + }, + }, + }, +}; + +// NOTE: Change these to match your setup. +const SSID = "SSID"; +const PASSWORD: []const u8 = ""; +const AUTH_METHOD: radio.wifi.AuthMethod = .none; +const SERVER_PORT = 3333; + +var maybe_netif: ?*lwip.c.netif = null; + +var ip_ready_semaphore: rtos.Semaphore = .init(0, 1); +var ip: lwip.c.ip_addr_t = undefined; + +extern fn netconn_new_with_proto_and_callback(t: lwip.c.enum_netconn_type, proto: lwip.c.u8_t, callback: ?*const anyopaque) [*c]lwip.c.struct_netconn; +pub fn main() !void { + var heap_allocator: microzig.Allocator = .init_with_heap(16384); + const gpa = heap_allocator.allocator(); + + try radio.wifi.init(gpa, .{}); + defer radio.wifi.deinit(); + + exports.gpa = gpa; + lwip.c.tcpip_init(null, null); + + var netif: lwip.c.netif = undefined; + _ = lwip.c.netifapi_netif_add( + &netif, + @ptrCast(lwip.c.IP4_ADDR_ANY), + @ptrCast(lwip.c.IP4_ADDR_ANY), + @ptrCast(lwip.c.IP4_ADDR_ANY), + null, + netif_init, + lwip.c.tcpip_input, + ); + maybe_netif = &netif; + try lwip.c_err(lwip.c.netifapi_netif_common(&netif, null, lwip.c.dhcp_start)); + + try radio.wifi.apply(.{ + .sta = .{ + .ssid = SSID, + .password = PASSWORD, + .auth_method = AUTH_METHOD, + }, + }); + try radio.wifi.start_blocking(); + + { + std.log.info("Scanning for access points...", .{}); + var scan_iter = try radio.wifi.scan(.{}); + defer scan_iter.deinit(); + while (try scan_iter.next()) |record| { + std.log.info("Found ap `{s}` RSSI: {} Channel: {}, Auth: {?t}", .{ + record.ssid, + record.rssi, + record.primary_channel, + record.auth_mode, + }); + } + } + + try radio.wifi.connect_blocking(); + try lwip.c_err(lwip.c.netifapi_netif_common(&netif, lwip.c.netif_set_link_up, null)); + + ip_ready_semaphore.take(); + std.log.info("Listening on {f}:{}", .{ IP_Formatter.init(netif.ip_addr), SERVER_PORT }); + + const server_conn = netconn_new_with_proto_and_callback(lwip.c.NETCONN_TCP, 0, null) orelse { + std.log.err("Failed to create netconn", .{}); + return error.FailedToCreateNetconn; + }; + defer { + _ = lwip.c.netconn_close(server_conn); + _ = lwip.c.netconn_delete(server_conn); + } + + try lwip.c_err(lwip.c.netconn_bind(server_conn, null, SERVER_PORT)); + + try lwip.c_err(lwip.c.netconn_listen(server_conn)); + std.log.info("TCP Server listening...", .{}); + + while (true) : (rtos.sleep(.from_ms(200))) { + var client_conn: ?*lwip.c.netconn = null; + + lwip.c_err(lwip.c.netconn_accept(server_conn, &client_conn)) catch |err| { + std.log.err("failed to accept connection: {t}", .{err}); + continue; + }; + defer { + _ = lwip.c.netconn_close(client_conn); + _ = lwip.c.netconn_delete(client_conn); + } + + std.log.info("New client connected!", .{}); + defer std.log.info("Client disconnected.", .{}); + handle_client(client_conn) catch |err| { + std.log.err("error while connected to client: {t}", .{err}); + continue; + }; + } +} + +fn handle_client(conn: ?*lwip.c.netconn) !void { + var buf: ?*lwip.c.netbuf = null; + while (true) { + lwip.c_err(lwip.c.netconn_recv(conn, &buf)) catch |err| switch (err) { + error.ConnectionClosed => break, + else => return err, + }; + defer lwip.c.netbuf_delete(buf); + + var data_ptr: ?*anyopaque = null; + var len: u16 = 0; + try lwip.c_err(lwip.c.netbuf_data(buf, &data_ptr, &len)); + + const slice = @as([*]u8, @ptrCast(data_ptr))[0..len]; + std.log.info("Received {} bytes: {s}", .{ len, slice }); + + // Echo back to client + try lwip.c_err(lwip.c.netconn_write_partly(conn, data_ptr, len, lwip.c.NETCONN_COPY, null)); + } +} + +fn on_packet_received(comptime _: radio.wifi.Interface, data: []const u8) void { + const pbuf: *lwip.c.struct_pbuf = lwip.c.pbuf_alloc(lwip.c.PBUF_RAW, @intCast(data.len), lwip.c.PBUF_POOL) orelse { + std.log.err("failed to allocate receive pbuf", .{}); + return; + }; + lwip.c_err(lwip.c.pbuf_take(pbuf, data.ptr, @intCast(data.len))) catch unreachable; + if (maybe_netif) |netif| { + lwip.c_err(netif.input.?(pbuf, netif)) catch |err| { + _ = lwip.c.pbuf_free(pbuf); + std.log.warn("tcpip_input failed: {t}", .{err}); + }; + } else { + _ = lwip.c.pbuf_free(pbuf); + } +} + +fn netif_init(netif_ptr: [*c]lwip.c.struct_netif) callconv(.c) lwip.c.err_t { + const netif = &netif_ptr[0]; + @memcpy(&netif.name, "w0"); + netif.linkoutput = netif_output; + netif.output = lwip.c.etharp_output; + netif.output_ip6 = lwip.c.ethip6_output; + netif.mtu = 1500; + netif.flags = lwip.c.NETIF_FLAG_BROADCAST | lwip.c.NETIF_FLAG_ETHARP | lwip.c.NETIF_FLAG_ETHERNET | lwip.c.NETIF_FLAG_IGMP | lwip.c.NETIF_FLAG_MLD6; + @memcpy(&netif.hwaddr, &radio.read_mac(.sta)); + netif.hwaddr_len = 6; + lwip.c.netif_create_ip6_linklocal_address(netif, 1); + lwip.c.netif_set_status_callback(netif, netif_status_callback); + lwip.c.netif_set_default(netif); + lwip.c.netif_set_up(netif); + return lwip.c.ERR_OK; +} + +fn netif_status_callback(netif_ptr: [*c]lwip.c.netif) callconv(.c) void { + const netif = &netif_ptr[0]; + if (netif.ip_addr.u_addr.ip4.addr != 0) { + ip = netif.ip_addr; + ip_ready_semaphore.give(); + } +} + +var packet_buf: [1600]u8 = undefined; + +fn netif_output(_: [*c]lwip.c.struct_netif, pbuf_c: [*c]lwip.c.struct_pbuf) callconv(.c) lwip.c.err_t { + const pbuf: *lwip.c.struct_pbuf = pbuf_c; + std.debug.assert(pbuf.tot_len < packet_buf.len); + + var off: usize = 0; + while (off < pbuf.tot_len) { + const cnt = lwip.c.pbuf_copy_partial(pbuf, packet_buf[off..].ptr, @as(u15, @intCast(pbuf.tot_len - off)), @as(u15, @intCast(off))); + if (cnt == 0) { + std.log.err("failed to copy network packet", .{}); + return lwip.c.ERR_BUF; + } + off += cnt; + } + + radio.wifi.send_packet(.sta, packet_buf[0..pbuf.tot_len]) catch |err| { + std.log.err("failed to send packet: {}", .{err}); + return lwip.c.ERR_IF; + }; + + return lwip.c.ERR_OK; +} + +const IP_Formatter = struct { + addr: lwip.c.ip_addr_t, + + pub fn init(addr: lwip.c.ip_addr_t) IP_Formatter { + return .{ .addr = addr }; + } + + pub fn format(addr: IP_Formatter, writer: *std.Io.Writer) !void { + try writer.writeAll(std.mem.sliceTo(lwip.c.ip4addr_ntoa(@as(*const lwip.c.ip4_addr_t, @ptrCast(&addr.addr))), 0)); + } +}; diff --git a/modules/foundation-libc/build.zig b/modules/foundation-libc/build.zig index 31ae1c883..47d6410ea 100644 --- a/modules/foundation-libc/build.zig +++ b/modules/foundation-libc/build.zig @@ -42,6 +42,7 @@ const header_files = [_][]const u8{ "inttypes.h", "math.h", "setjmp.h", + "stdio.h", "stdlib.h", "string.h", "tgmath.h", diff --git a/modules/foundation-libc/include/stdio.h b/modules/foundation-libc/include/stdio.h new file mode 100644 index 000000000..a0b7e90cf --- /dev/null +++ b/modules/foundation-libc/include/stdio.h @@ -0,0 +1,6 @@ +#ifndef _FOUNDATION_LIBC_STDIO_H_ +#define _FOUNDATION_LIBC_STDIO_H_ + +// empty for now + +#endif diff --git a/modules/foundation-libc/test/build.zig b/modules/foundation-libc/test/build.zig index a0266ba88..ef5c6759f 100644 --- a/modules/foundation-libc/test/build.zig +++ b/modules/foundation-libc/test/build.zig @@ -172,7 +172,7 @@ const validation_target_list = [_]std.Target.Query{ .{ .cpu_arch = .wasm64, .os_tag = .freestanding }, // nice to have, but broken: - //.{ .cpu_arch = .avr, .os_tag = .freestanding }, + // .{ .cpu_arch = .avr, .os_tag = .freestanding }, // .{ .cpu_arch = .msp430, .os_tag = .freestanding }, // error: unknown target CPU 'generic' // .{ .cpu_arch = .m68k, .os_tag = .freestanding }, // .{ .cpu_arch = .xtensa, .os_tag = .freestanding }, diff --git a/modules/lwip/build.zig b/modules/lwip/build.zig index e53a1faa2..cfea858e3 100644 --- a/modules/lwip/build.zig +++ b/modules/lwip/build.zig @@ -4,11 +4,22 @@ pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + const maybe_include_dir = b.option( + std.Build.LazyPath, + "include_dir", + "Configurable include directory for lwip", + ); + const upstream = b.dependency("lwip", .{}); - const lwip = b.addModule("lwip", .{ - .target = target, - .optimize = optimize, + const lwip = b.addLibrary(.{ + .name = "lwip", + .root_module = b.createModule(.{ + .optimize = optimize, + .target = target, + .link_libc = false, + }), + .linkage = .static, }); lwip.addCSourceFiles(.{ @@ -17,13 +28,16 @@ pub fn build(b: *std.Build) void { .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")); -// } + if (maybe_include_dir) |include_dir| { + lwip.addIncludePath(include_dir); + lwip.installHeadersDirectory(include_dir, "", .{}); + } + + lwip.installHeadersDirectory(upstream.path("src/include"), "", .{}); + + b.installArtifact(lwip); +} const flags = [_][]const u8{ "-std=c99", "-fno-sanitize=undefined" }; const files = [_][]const u8{ @@ -77,15 +91,15 @@ const files = [_][]const u8{ "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", + "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", diff --git a/modules/network/build.zig b/modules/network/build.zig index b777f7321..77e4594b6 100644 --- a/modules/network/build.zig +++ b/modules/network/build.zig @@ -37,37 +37,6 @@ pub fn build(b: *std.Build) void { @panic("Invalid lwip packet buffer length"); } - const lwip_mod = brk: { - var buf: [32]u8 = undefined; - - const foundation_dep = b.dependency("foundationlibc", .{ - .target = target, - .optimize = optimize, - .single_threaded = true, - }); - 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")); - - // MEM_ALIGNMENT of 4 bytes allows us to use packet buffers in u32 dma. - lwip_mod.addCMacro("MEM_ALIGNMENT", "4"); - lwip_mod.addCMacro("MEM_SIZE", to_s(&buf, mem_size)); - lwip_mod.addCMacro("PBUF_POOL_SIZE", to_s(&buf, pbuf_pool_size)); - lwip_mod.addCMacro("PBUF_LINK_HLEN", to_s(&buf, ethernet_header)); - lwip_mod.addCMacro("PBUF_POOL_BUFSIZE", to_s(&buf, pbuf_length)); - lwip_mod.addCMacro("PBUF_LINK_ENCAPSULATION_HLEN", to_s(&buf, pbuf_header_length)); - // 40 bytes IPv6 header, 20 bytes TCP header - lwip_mod.addCMacro("TCP_MSS", to_s(&buf, mtu - 40 - 20)); - - // Path to lwipopts.h - lwip_mod.addIncludePath(b.path("src/include")); - break :brk lwip_mod; - }; - const net_mod = b.addModule("net", .{ .target = target, .optimize = optimize, @@ -77,15 +46,34 @@ pub fn build(b: *std.Build) void { .module = b.dependency("link", .{}).module("link"), }}, }); - net_mod.addImport("lwip", lwip_mod); - // Copy macros and include dirs from lwip to net, so we have same values - // when calling translate-c from cImport. - for (lwip_mod.c_macros.items) |m| { - net_mod.c_macros.append(b.allocator, m) catch @panic("out of memory"); - } - for (lwip_mod.include_dirs.items) |dir| { - net_mod.include_dirs.append(b.allocator, dir) catch @panic("out of memory"); - } + + var buf: [32]u8 = undefined; + + const foundation_dep = b.dependency("foundationlibc", .{ + .target = target, + .optimize = optimize, + .single_threaded = true, + }); + const lwip_dep = b.dependency("lwip", .{ + .target = target, + .optimize = optimize, + .include_dir = b.path("src/include"), + }); + const lwip_lib = lwip_dep.artifact("lwip"); + + lwip_lib.root_module.linkLibrary(foundation_dep.artifact("foundation")); + net_mod.linkLibrary(lwip_lib); + + // MEM_ALIGNMENT of 4 bytes allows us to use packet buffers in u32 dma. + net_mod.addCMacro("MEM_ALIGNMENT", "4"); + net_mod.addCMacro("MEM_SIZE", to_s(&buf, mem_size)); + net_mod.addCMacro("PBUF_POOL_SIZE", to_s(&buf, pbuf_pool_size)); + net_mod.addCMacro("PBUF_LINK_HLEN", to_s(&buf, ethernet_header)); + net_mod.addCMacro("PBUF_POOL_BUFSIZE", to_s(&buf, pbuf_length)); + net_mod.addCMacro("PBUF_LINK_ENCAPSULATION_HLEN", to_s(&buf, pbuf_header_length)); + // 40 bytes IPv6 header, 20 bytes TCP header + net_mod.addCMacro("TCP_MSS", to_s(&buf, mtu - 40 - 20)); + const options = b.addOptions(); options.addOption(u16, "mtu", mtu); options.addOption(u16, "pbuf_length", pbuf_length); diff --git a/modules/riscv32-common/src/riscv32_common.zig b/modules/riscv32-common/src/riscv32_common.zig index 50800d448..72a1da7be 100644 --- a/modules/riscv32-common/src/riscv32_common.zig +++ b/modules/riscv32-common/src/riscv32_common.zig @@ -53,6 +53,10 @@ pub fn wfi() void { asm volatile ("wfi"); } +pub fn fence() void { + asm volatile ("fence"); +} + // NOTE: Contains all CSRs (Control Status Registers) from the riscv manual and should follow their // spec. Cpu implementations can reexport what they need from here. // See https://docs.riscv.org/reference/isa/priv/priv-csrs.html diff --git a/port/espressif/esp/build.zig b/port/espressif/esp/build.zig index 4e53a0ac1..9f9519ee0 100644 --- a/port/espressif/esp/build.zig +++ b/port/espressif/esp/build.zig @@ -15,21 +15,24 @@ boards: struct {}, pub fn init(dep: *std.Build.Dependency) Self { const b = dep.builder; + const esp32_c3_zig_target: std.Target.Query = .{ + .cpu_arch = .riscv32, + .cpu_model = .{ .explicit = &std.Target.riscv.cpu.generic_rv32 }, + .cpu_features_add = std.Target.riscv.featureSet(&.{ + .c, + .m, + }), + .os_tag = .freestanding, + .abi = .eabi, + }; + const riscv32_common_dep = b.dependency("microzig/modules/riscv32-common", .{}); const riscv32_common_mod = riscv32_common_dep.module("riscv32-common"); const esp_image_dep = b.dependency("microzig/tools/esp-image", .{}); const esp_image_mod = esp_image_dep.module("esp_image"); - const hal: microzig.HardwareAbstractionLayer = .{ - .root_source_file = b.path("src/hal.zig"), - .imports = b.allocator.dupe(std.Build.Module.Import, &.{ - .{ - .name = "esp_image", - .module = esp_image_mod, - }, - }) catch @panic("OOM"), - }; + const esp_wifi_driver_mod = make_esp_wifi_driver_module(b, "esp32c3", esp32_c3_zig_target); const chip_esp32_c3: microzig.Target = .{ .dep = dep, @@ -39,16 +42,7 @@ pub fn init(dep: *std.Build.Dependency) Self { .flash_size = .@"4mb", .flash_freq = .@"40m", } }, - .zig_target = .{ - .cpu_arch = .riscv32, - .cpu_model = .{ .explicit = &std.Target.riscv.cpu.generic_rv32 }, - .cpu_features_add = std.Target.riscv.featureSet(&.{ - .c, - .m, - }), - .os_tag = .freestanding, - .abi = .eabi, - }, + .zig_target = esp32_c3_zig_target, .cpu = .{ .name = "esp_riscv", .root_source_file = b.path("src/cpus/esp_riscv.zig"), @@ -75,7 +69,19 @@ pub fn init(dep: *std.Build.Dependency) Self { .{ .name = "DRAM", .tag = .ram, .offset = 0x3FC7C000 + 0x4000, .length = 313 * 1024, .access = .rw }, }, }, - .hal = hal, + .hal = .{ + .root_source_file = b.path("src/hal.zig"), + .imports = b.allocator.dupe(std.Build.Module.Import, &.{ + .{ + .name = "esp_image", + .module = esp_image_mod, + }, + .{ + .name = "esp-wifi-driver", + .module = esp_wifi_driver_mod, + }, + }) catch @panic("OOM"), + }, .linker_script = .{ .generate = .memory_regions, .file = generate_linker_script( @@ -187,3 +193,48 @@ fn generate_linker_script( run.addFileArg(rom_functions_path); return run.addOutputFileArg(output_name); } + +fn make_esp_wifi_driver_module(b: *std.Build, chip_name: []const u8, target_query: std.Target.Query) *std.Build.Module { + const esp_wifi_sys_dep = b.dependency("esp-wifi-sys", .{}); + + const esp32_c3_resolved_zig_target = b.resolveTargetQuery(target_query); + const translate_c = b.addTranslateC(.{ + .root_source_file = esp_wifi_sys_dep.path("c/include/include.h"), + .target = esp32_c3_resolved_zig_target, + .optimize = .Debug, + .link_libc = false, + }); + + translate_c.addIncludePath(b.path("src/hal/radio/libc_dummy_include")); + translate_c.addIncludePath(esp_wifi_sys_dep.path("c/headers")); + translate_c.addIncludePath(esp_wifi_sys_dep.path("c/include")); + translate_c.addIncludePath(esp_wifi_sys_dep.path(b.fmt("c/include/{s}", .{chip_name}))); + translate_c.addIncludePath(esp_wifi_sys_dep.path(b.fmt("c/headers/{s}", .{chip_name}))); + + const mod = translate_c.addModule("esp-wifi-driver"); + mod.addLibraryPath(esp_wifi_sys_dep.path(b.fmt("esp-wifi-sys-{s}/libs", .{chip_name}))); + + inline for (&.{ + "btbb", + "btdm_app", + "coexist", + "core", + "espnow", + "mesh", + "net80211", + "phy", + "pp", + "regulatory", + "smartconfig", + "wapi", + "wpa_supplicant", + }) |library| { + mod.linkSystemLibrary(library, .{}); + } + + mod.linkSystemLibrary("printf", .{ + .weak = true, + }); + + return mod; +} diff --git a/port/espressif/esp/build.zig.zon b/port/espressif/esp/build.zig.zon index 35f3d77d1..45d32550d 100644 --- a/port/espressif/esp/build.zig.zon +++ b/port/espressif/esp/build.zig.zon @@ -6,6 +6,10 @@ .@"microzig/build-internals" = .{ .path = "../../../build-internals" }, .@"microzig/modules/riscv32-common" = .{ .path = "../../../modules/riscv32-common" }, .@"microzig/tools/esp-image" = .{ .path = "../../../tools/esp-image" }, + .@"esp-wifi-sys" = .{ + .url = "git+https://github.com/esp-rs/esp-wifi-sys#7623c8d746b55cd8d9f7473359069aef381b7d3b", + .hash = "N-V-__8AANWs5wTMnVsq2oG-zKWSf91LJ6q8Vv6EUD6xNbKJ", + }, }, .paths = .{ "README.md", diff --git a/port/espressif/esp/ld/esp32_c3/direct_boot_sections.ld b/port/espressif/esp/ld/esp32_c3/direct_boot_sections.ld index c52319061..770c6f2ba 100644 --- a/port/espressif/esp/ld/esp32_c3/direct_boot_sections.ld +++ b/port/espressif/esp/ld/esp32_c3/direct_boot_sections.ld @@ -5,9 +5,9 @@ SECTIONS _irom_start = .; KEEP(*(microzig_flash_start)) *(.text*) - } > IROM - . = ALIGN(8); + . = ALIGN(8); + } > IROM _irom_size = . - _irom_start; @@ -15,38 +15,72 @@ SECTIONS .rodata _drom_start : AT(_irom_size) { *(.rodata*) + *(.srodata*) + + /* wifi rodata */ + *(.rodata_wlog_*.*) } > DROM _drom_size = . - _drom_start; .ram_text ORIGIN(IRAM) : AT(_irom_size + _drom_size) { + . = ALIGN(16); microzig_ram_text_start = .; KEEP(*(.ram_text)) + KEEP(*(.ram_vectors)) + + . = ALIGN(16); + /* wifi rwtext */ + *(.wifi0iram .wifi0iram.*) + *(.wifirxiram .wifirxiram.*) + *(.wifislprxiram .wifislprxiram.*) + *(.wifislpiram .wifislpiram.*) + *(.phyiram .phyiram.*) + *(.iram1 .iram1.*) + *(.wifiextrairam.* ) + *(.coexiram.* ) + + . = ALIGN(16); microzig_ram_text_end = .; } > IRAM - . = ALIGN(4); + . = ALIGN(16); _iram_size = . - microzig_ram_text_start; _dram_start = ORIGIN(DRAM) + _iram_size; .data _dram_start : AT(_irom_size + _drom_size + _iram_size) { + . = ALIGN(16); /* put microzig_data_start right before .ram_text */ microzig_data_start = . - _iram_size; *(.sdata*) *(.data*) + + /* wifi data */ + *(.dram1*) + + . = ALIGN(16); microzig_data_end = .; } > DRAM .bss (NOLOAD) : { + . = ALIGN(16); microzig_bss_start = .; - *(.bss*) *(.sbss*) + *(.bss*) + . = ALIGN(16); microzig_bss_end = .; } > DRAM + .heap (NOLOAD) : + { + microzig_heap_start = .; + . = ORIGIN(DRAM) + LENGTH(DRAM); + microzig_heap_end = .; + } > DRAM + microzig_data_load_start = ORIGIN(DROM) + _irom_size + _drom_size; PROVIDE(__global_pointer$ = microzig_data_start + 0x800); } diff --git a/port/espressif/esp/ld/esp32_c3/flashless_sections.ld b/port/espressif/esp/ld/esp32_c3/flashless_sections.ld index 160605635..833eaf141 100644 --- a/port/espressif/esp/ld/esp32_c3/flashless_sections.ld +++ b/port/espressif/esp/ld/esp32_c3/flashless_sections.ld @@ -7,27 +7,55 @@ SECTIONS KEEP(*(microzig_ram_start)) *(.text*) KEEP(*(.ram_text)) + KEEP(*(.ram_vectors)) + + /* wifi rwtext */ + *(.wifi0iram .wifi0iram.*) + *(.wifirxiram .wifirxiram.*) + *(.wifislprxiram .wifislprxiram.*) + *(.wifislpiram .wifislpiram.*) + *(.phyiram .phyiram.*) + *(.iram1 .iram1.*) + *(.wifiextrairam.* ) + *(.coexiram.* ) } > IRAM - . = ALIGN(8); + . = ALIGN(16); _iram_size = . - microzig_ram_text_start; _dram_start = ORIGIN(DRAM) + _iram_size; .data _dram_start : AT(_iram_size) { + . = ALIGN(16); microzig_data_start = .; + *(.srodata*) *(.sdata*) - *(.data*) *(.rodata*) + *(.data*) + + /* wifi rodata */ + *(.rodata_wlog_*.*) + + /* wifi data */ + *(.dram1 .dram1.*) } > DRAM .bss (NOLOAD) : { + . = ALIGN(16); microzig_bss_start = .; *(.bss*) *(.sbss*) + . = ALIGN(16); microzig_bss_end = .; } > DRAM + .heap (NOLOAD) : + { + microzig_heap_start = .; + . = ORIGIN(DRAM) + LENGTH(DRAM); + microzig_heap_end = .; + } > DRAM + PROVIDE(__global_pointer$ = microzig_data_start + 0x800); } diff --git a/port/espressif/esp/ld/esp32_c3/image_boot_sections.ld b/port/espressif/esp/ld/esp32_c3/image_boot_sections.ld index 6edd5544b..bcc305a03 100644 --- a/port/espressif/esp/ld/esp32_c3/image_boot_sections.ld +++ b/port/espressif/esp/ld/esp32_c3/image_boot_sections.ld @@ -42,36 +42,77 @@ SECTIONS .flash.rodata : ALIGN(0x10) { *(.rodata*) + *(.srodata*) + + . = ALIGN(ALIGNOF(.flash.rodata.wifi)); + } > DROM + + .flash.rodata.wifi : ALIGN(0x10) + { + *(.rodata_wlog_*.*) } > DROM .ram.text : { + . = ALIGN(16); KEEP(*(.ram_text)) + KEEP(*(.ram_vectors)) /* TODO: in the case of memory protection there should be some alignment * and offset done here (NOLOAD) */ } > IRAM + .ram.text.wifi : + { + . = ALIGN(16); + *( .wifi0iram .wifi0iram.*) + *( .wifirxiram .wifirxiram.*) + *( .wifislprxiram .wifislprxiram.*) + *( .wifislpiram .wifislpiram.*) + *( .phyiram .phyiram.*) + *( .iram1 .iram1.*) + *( .wifiextrairam.* ) + *( .coexiram.* ) + } > IRAM + .ram_data_dummy (NOLOAD) : { . = ALIGN(ALIGNOF(.ram.text)) + SIZEOF(.ram.text); + . = ALIGN(ALIGNOF(.ram.text.wifi)) + SIZEOF(.ram.text.wifi); } > DRAM .data : { + . = ALIGN(16); microzig_data_start = .; *(.sdata*) *(.data*) + . = ALIGN(16); microzig_data_end = .; } > DRAM + .data.wifi : + { + . = ALIGN(16); + *(.dram1*) + } > DRAM + .bss (NOLOAD) : { + . = ALIGN(16); microzig_bss_start = .; *(.sbss*) *(.bss*) + . = ALIGN(16); microzig_bss_end = .; } > DRAM + .heap (NOLOAD) : + { + microzig_heap_start = .; + . = ORIGIN(DRAM) + LENGTH(DRAM); + microzig_heap_end = .; + } > DRAM + PROVIDE(__global_pointer$ = microzig_data_start + 0x800); } diff --git a/port/espressif/esp/ld/esp32_c3/rom_functions.ld b/port/espressif/esp/ld/esp32_c3/rom_functions.ld index 91dfce120..5becb1fa5 100644 --- a/port/espressif/esp/ld/esp32_c3/rom_functions.ld +++ b/port/espressif/esp/ld/esp32_c3/rom_functions.ld @@ -1,26 +1,3 @@ -/* Taken from https://github.com/esp-rs/esp-hal/tree/537ac73c3ad7e056c430474ff78cb1ae8df668d0 */ - -memset = 0x40000354; -memcpy = 0x40000358; -memmove = 0x4000035c; -memcmp = 0x40000360; - -strncmp = 0x40000370; -strncpy = 0x40000368; -strcpy = 0x40000364; - -abs = 0x40000424; - -PROVIDE(cache_dbus_mmu_set = 0x40000564); - -PROVIDE( strcat = 0x400003d8 ); -PROVIDE( strcmp = 0x4000036c ); -PROVIDE( strchr = 0x400003e0 ); -PROVIDE( strlcpy = 0x400003f0 ); -PROVIDE( strstr = 0x40000378 ); -PROVIDE( strcasecmp = 0x400003d0 ); - -PROVIDE( memchr = 0x400003c8 ); /** * ROM APIs */ @@ -82,6 +59,7 @@ PROVIDE ( esp_rom_regi2c_read = rom_i2c_readReg ); PROVIDE ( esp_rom_regi2c_read_mask = rom_i2c_readReg_Mask ); PROVIDE ( esp_rom_regi2c_write = rom_i2c_writeReg ); PROVIDE ( esp_rom_regi2c_write_mask = rom_i2c_writeReg_Mask ); + /* ESP32C3 ECO3 ROM address table Version 3 API's imported from the ROM @@ -211,246 +189,7 @@ phy_param_rom = 0x3fcdf830; PROVIDE( esp_flash_read_chip_id = 0x40001c38 ); PROVIDE( detect_spi_flash_chip = 0x40001c3c ); PROVIDE( esp_rom_spiflash_write_disable = 0x40001c40 ); -/* - * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ -esf_buf_alloc = 0x400015bc; -esf_buf_alloc_dynamic = 0x400015c0; -esf_buf_recycle = 0x400015c4; -/*hal_mac_tx_set_ppdu = 0x400015d4;*/ -ic_mac_deinit = 0x400015dc; -lmacDiscardMSDU = 0x400015f4; -/*lmacSetTxFrame = 0x40001628;*/ -lmacTxDone = 0x4000162c; -lmacTxFrame = 0x40001630; -mac_tx_set_htsig = 0x40001638; -mac_tx_set_plcp1 = 0x40001640; -pm_check_state = 0x40001648; -/*pm_on_beacon_rx = 0x4000167c;*/ -/*pm_parse_beacon = 0x40001688;*/ -pm_process_tim = 0x4000168c; -pm_rx_beacon_process = 0x40001690; -pm_rx_data_process = 0x40001694; -/* pm_sleep = 0x40001698;*/ -/* pm_tbtt_process = 0x400016a0;*/ -ppMapTxQueue = 0x400016d8; -ppProcTxSecFrame = 0x400016dc; -/*ppRxFragmentProc = 0x40001704;*/ -/* rcGetSched = 0x40001764;*/ -rcTxUpdatePer = 0x40001770; -rcUpdateTxDone = 0x4000177c; -wDevCheckBlockError = 0x400017b4; -/* wDev_IndicateFrame = 0x400017c8;*/ -wDev_ProcessFiq = 0x400017f0; -/*wDev_ProcessRxSucData = 0x400017f4;*/ -/*ppProcTxDone = 0x40001804;*/ -/*pm_tx_data_done_process = 0x40001808;*/ -ppMapWaitTxq = 0x40001810; -/*ieee80211_encap_esfbuf = 0x4000185c;*/ -/*sta_input = 0x40001870;*/ -ieee80211_crypto_decap = 0x4000189c; -ieee80211_decap = 0x400018a0; -/*coex_core_timer_idx_get = 0x400018d0;*/ -rom1_chip_i2c_readReg = 0x40001924; -rom1_chip_i2c_writeReg = 0x40001928; -rom_index_to_txbbgain = 0x40001964; -rom_pbus_xpd_tx_on = 0x400019b0; -rom1_set_noise_floor = 0x400019e8; -rom_set_tx_dig_gain = 0x400019f0; -rom_set_txcap_reg = 0x400019f4; -rom_txbbgain_to_index = 0x40001a0c; -rom1_disable_wifi_agc = 0x40001a1c; -rom1_enable_wifi_agc = 0x40001a20; -rom1_tx_paon_set = 0x40001a44; -rom_agc_reg_init = 0x40001a54; -rom_bb_reg_init = 0x40001a58; -rom1_set_pbus_reg = 0x40001a70; -rom_phy_xpd_rf = 0x40001a78; -rom_write_txrate_power_offset = 0x40001a8c; -rom1_get_rate_fcc_index = 0x40001a90; -rom1_read_sar2_code = 0x40001aa4; -rom2_temp_to_power1 = 0x40001ab4; -rom1_get_i2c_hostid = 0x40001ac8; -rom_open_i2c_xpd = 0x40001af8; -rom2_tsens_read_init1 = 0x40001b00; -rom_tsens_code_read = 0x40001b04; -rom_tsens_dac_cal = 0x40001b10; -rom1_phy_en_hw_set_freq = 0x40001b20; -rom1_phy_dis_hw_set_freq = 0x40001b24; -rom_pll_vol_cal = 0x40001b28; - -rom1_bt_get_tx_gain = 0x40001bb8; -rom1_get_chan_target_power = 0x40001bbc; -rom2_get_tx_gain_value1 = 0x40001bc0; -rom1_wifi_tx_dig_gain = 0x40001bc4; -rom1_wifi_get_tx_gain = 0x40001bc8; -rom1_fe_i2c_reg_renew = 0x40001bcc; -rom1_i2c_master_reset = 0x40001bd4; -rom1_phy_wakeup_init = 0x40001bf0; -rom1_phy_i2c_init1 = 0x40001bf4; -rom1_tsens_temp_read = 0x40001bf8; -rom1_bt_track_pll_cap = 0x40001bfc; -rom1_wifi_set_tx_gain = 0x40001c04; -rom1_txpwr_cal_track = 0x40001c08; -rom1_bt_set_tx_gain = 0x40001c10; -rom1_phy_close_rf = 0x40001c18; - - -/*************************************** - Group eco7_uart - ***************************************/ - -/* Functions */ -uart_tx_switch = 0x40001c44; - - -/*************************************** - Group eco7_bluetooth - ***************************************/ -/* Functions */ -r_lld_con_count_get = 0x40001c48; -r_lld_update_con_offset = 0x40001c4c; -r_lld_con_update_last_clock = 0x40001c50; -r_lld_con_llcp_ind_info_clear = 0x40001c54; -r_lld_con_update_terminte_info_init = 0x40001c58; -r_lld_con_terminate_max_evt_update = 0x40001c5c; -r_llc_pref_param_compute_eco = 0x40001ce8; -r_llc_hci_con_upd_info_send_eco = 0x40001cec; -r_llc_rem_encrypt_proc_continue_eco = 0x40001cf0; -r_llc_start_eco = 0x40001cf8; -r_lld_ext_adv_dynamic_aux_pti_process_eco = 0x40001cfc; -r_lld_adv_start_eco = 0x40001d04; -r_lld_con_evt_canceled_cbk_eco = 0x40001d08; -r_lld_con_evt_time_update_eco = 0x40001d0c; -r_lld_con_start_eco = 0x40001d10; -r_lld_con_frm_isr_eco = 0x40001d14; -r_lld_con_tx_eco = 0x40001d18; -r_lld_ext_scan_dynamic_pti_process_eco = 0x40001d28; -r_lld_scan_frm_eof_isr_eco = 0x40001d2c; -r_lld_sync_start_eco = 0x40001d30; -r_lld_sync_insert_eco = 0x40001d34; -r_llm_adv_rep_flow_control_update_eco = 0x40001d38; -r_llm_env_adv_dup_filt_init_eco = 0x40001d3c; -r_llm_env_adv_dup_filt_deinit_eco = 0x40001d40; -r_llm_adv_rep_flow_control_check_eco = 0x40001d44; -r_llm_scan_start_eco = 0x40001d48; -r_llm_update_duplicate_scan_count = 0x40001d4c; -r_llc_hci_command_handler_pre = 0x40001d50; -r_llc_hci_command_handler_get = 0x40001d54; -r_llc_hci_command_handler_search = 0x40001d58; -r_llc_llcp_pdu_handler_get_overwrite = 0x40001d5c; -r_llc_llcp_pdu_handler_pre = 0x40001d60; -r_llc_llcp_pdu_handler_end = 0x40001d64; -r_llc_con_conflict_check = 0x40001d6c; -r_sch_prog_hw_reset_try = 0x40001d70; -r_sch_prog_et_state_reset = 0x40001d74; -r_sch_prog_end_isr_handler = 0x40001d78; -r_sch_plan_conflict_check = 0x40001d7c; -r_rwble_isr_hw_fixed = 0x40001d80; -r_bt_bb_recorrect_is_dead = 0x40001d84; -r_bt_bb_restart_hw_recorrect = 0x40001d88; -r_ke_task_handler_pre = 0x40001da0; -r_ke_task_handler_end = 0x40001da4; -r_lld_scan_frm_skip_isr_eco = 0x40001db0; -r_lld_ext_scan_dynamic_pti_reset = 0x40001db4; -r_llc_rem_phy_upd_proc_continue_eco = 0x40001db8; -r_llm_get_preferred_phys = 0x40001dbc; -r_lld_hw_cca_isr_eco = 0x40001dc0; -r_lld_sw_cca_isr_eco = 0x40001dc4; -r_lld_cca_chan_prn_e = 0x40001dc8; -r_lld_cca_chan_prn_s = 0x40001dcc; -r_lld_cca_chan_sel_remap = 0x40001dd0; -r_lld_cca_chan_sel_1 = 0x40001dd4; -r_lld_cca_chan_sel_2 = 0x40001dd8; -r_lld_cca_set_thresh = 0x40001ddc; -r_lld_cca_con_start = 0x40001de0; -r_lld_cca_con_end = 0x40001de4; -r_lld_cca_chm_restore = 0x40001de8; -r_lld_cca_chan_unused_check = 0x40001dec; -r_lld_cca_chm_update_check = 0x40001df0; -r_lld_cca_busy_mode_handle = 0x40001df4; -r_lld_cca_lbt_handle = 0x40001df8; -r_lld_cca_scst_timeout_check = 0x40001dfc; -r_lld_cca_chan_avl_timeout_check = 0x40001e00; - -r_lld_con_start_hook = 0x40001ca8; - -/* ble Functions eco */ -r_bt_bb_isr = 0x40000b9c; -r_bt_rf_coex_conn_phy_coded_data_time_limit_en_get = 0x40000ba8; -r_bt_rtp_get_txpwr_idx_by_act = 0x40000c00; -r_btdm_task_post = 0x40000c14; -r_btdm_task_post_from_isr = 0x40000c18; -r_btdm_task_recycle = 0x40000c1c; -r_hci_register_vendor_desc_tab = 0x40000d9c; -r_ke_task_schedule = 0x40000e80; -r_llc_hci_command_handler = 0x40000ef0; -r_llc_loc_con_upd_proc_continue = 0x40000f60; -r_llc_loc_phy_upd_proc_continue = 0x40000f78; -r_llc_rem_con_upd_proc_continue = 0x40000fb4; -r_lld_con_sched = 0x40001118; -r_lld_con_stop = 0x40001124; -r_lld_llcp_rx_ind_handler = 0x400011b0; -r_lld_per_adv_sched = 0x400011f8; -r_lld_scan_process_pkt_rx_adv_rep = 0x40001284; -r_register_esp_vendor_cmd_handler = 0x40001400; -r_rf_txpwr_cs_get = 0x40001428; -r_rf_txpwr_dbm_get = 0x4000142c; -r_sch_arb_event_start_isr = 0x400014f8; -r_sch_plan_set = 0x40001534; -r_sch_prog_end_isr = 0x40001538; -r_lld_adv_ext_chain_scannable_construct = 0x40001b58; - -r_lld_scan_process_pkt_rx = 0x40001280; -r_llm_le_features_get = 0x400013b0; - -/* ble functions rename */ -r_lld_init_start_hack = 0x400011a4; - -/* ble functions disable */ -/* -r_lld_adv_frm_isr_eco = 0x40001d00; -r_lld_res_list_clear = 0x40004638; -r_lld_res_list_rem = 0x40004680; -r_lld_adv_start_hook = 0x40001c80; -r_lld_con_evt_start_cbk_eco = 0x40001d1c; -r_lld_con_tx_prog_new_packet = 0x40001b74; -r_lld_adv_ext_chain_none_construct = 0x40001b50; -r_llc_llcp_send_eco = 0x40001cf4; -r_llc_llcp_channel_map_ind_ack = 0x40001d68; -r_rwble_isr = 0x40001464; -r_lld_scan_start_eco = 0x40001d24; -r_lld_scan_try_sched_eco = 0x40001dac; -r_lld_scan_start_hook = 0x40001c74; -r_lld_init_start_hook = 0x40001cb8; -r_lld_scan_evt_start_cbk_eco = 0x40001d20; -r_ke_task_handler_get_overwrite = 0x40001da8; -*/ - - -/*************************************** - Group eco7_phy - ***************************************/ - -/* Functions */ -rom2_pll_cap_mem_update = 0x40001e04; -rom2_phy_i2c_enter_critical = 0x40001e08; -rom2_phy_i2c_exit_critical = 0x40001e0c; -rom2_rfpll_cap_correct = 0x40001e10; -rom2_write_pll_cap = 0x40001e14; -rom2_read_pll_cap = 0x40001e18; -rom2_tester_wifi_cali = 0x40001e1c; -rom2_wait_hw_freq_busy = 0x40001e20; -rom2_rfpll_cap_track = 0x40001e24; -rom2_ulp_code_track = 0x40001e28; -rom2_ulp_ext_code_set = 0x40001e2c; -rom2_phy_set_tsens_power = 0x40001e30; -rom2_phy_get_tsens_value = 0x40001e34; -rom_mac_tx_chan_offset = 0x40001e38; -rom_rx_gain_force = 0x40001e3c; /* ROM function interface esp32c3.rom.ld for esp32c3 * * @@ -2383,6 +2122,7 @@ rom_pll_correct_dcap = 0x40001b1c; rom_phy_en_hw_set_freq = 0x40001b20; rom_phy_dis_hw_set_freq = 0x40001b24; /* rom_pll_vol_cal = 0x40001b28; */ + /* ROM function interface esp32c3.rom.libgcc.ld for esp32c3 * * @@ -2488,6 +2228,7 @@ __umoddi3 = 0x400008bc; __umodsi3 = 0x400008c0; __unorddf2 = 0x400008c4; __unordsf2 = 0x400008c8; + /* ROM version variables for esp32c3 * * These addresses should be compatible with any ROM version for this chip. @@ -2496,3 +2237,25 @@ __unordsf2 = 0x400008c8; */ _rom_chip_id = 0x40000010; _rom_eco_version = 0x40000014; + +memset = 0x40000354; +memcpy = 0x40000358; +memmove = 0x4000035c; +memcmp = 0x40000360; + +strncmp = 0x40000370; +strncpy = 0x40000368; +strcpy = 0x40000364; + +abs = 0x40000424; + +PROVIDE(cache_dbus_mmu_set = 0x40000564); + +PROVIDE( strcat = 0x400003d8 ); +PROVIDE( strcmp = 0x4000036c ); +PROVIDE( strchr = 0x400003e0 ); +PROVIDE( strlcpy = 0x400003f0 ); +PROVIDE( strstr = 0x40000378 ); +PROVIDE( strcasecmp = 0x400003d0 ); + +PROVIDE( memchr = 0x400003c8 ); diff --git a/port/espressif/esp/src/cpus/esp_riscv.zig b/port/espressif/esp/src/cpus/esp_riscv.zig index 700a02bdb..dd578576b 100644 --- a/port/espressif/esp/src/cpus/esp_riscv.zig +++ b/port/espressif/esp/src/cpus/esp_riscv.zig @@ -4,6 +4,20 @@ const microzig = @import("microzig"); const cpu_config = @import("cpu-config"); const riscv32_common = @import("riscv32-common"); +const interrupt_stack_options = microzig.options.cpu.interrupt_stack; + +pub const CPU_Options = struct { + /// If not enabled, interrupts will use same stack. Otherwise, the + /// interrupt handler will switch to a custom stack after pushing the + /// TrapFrame. Nested interrupts use the interrupt stack as well. This + /// feature uses mscratch to store the old stack pointer. While not in + /// an interrupt, mscratch must be zero. + interrupt_stack: struct { + enable: bool = false, + size: usize = 4096, + } = .{}, +}; + pub const Exception = enum(u5) { InstructionFault = 0x1, IllegalInstruction = 0x2, @@ -48,7 +62,14 @@ pub const Interrupt = enum(u5) { interrupt31 = 31, }; -pub const InterruptHandler = *const fn (*TrapFrame) callconv(.c) void; +pub const InterruptHandler = union(enum) { + /// No state is saved. Do everything yourself. Interrupts are disabled + /// while it is executing. + naked: *const fn () callconv(.naked) void, + /// State pushed on the stack. Handler can be interrupted by higher + /// priority interrupts. + c: *const fn (*TrapFrame) callconv(.c) void, +}; pub const InterruptOptions = microzig.utilities.GenerateInterruptOptions(&.{ .{ .InterruptEnum = enum { Exception }, .HandlerFn = InterruptHandler }, @@ -57,8 +78,16 @@ pub const InterruptOptions = microzig.utilities.GenerateInterruptOptions(&.{ pub const interrupt = struct { pub const globally_enabled = riscv32_common.interrupt.globally_enabled; - pub const enable_interrupts = riscv32_common.interrupt.enable_interrupts; - pub const disable_interrupts = riscv32_common.interrupt.disable_interrupts; + + pub fn enable_interrupts() void { + fence(); + csr.mstatus.set(.{ .mie = 1 }); + } + + pub fn disable_interrupts() void { + csr.mstatus.clear(.{ .mie = 1 }); + fence(); + } const INTERRUPT_CORE0 = microzig.chip.peripherals.INTERRUPT_CORE0; @@ -226,10 +255,32 @@ pub const interrupt = struct { const base: usize = @intFromPtr(&INTERRUPT_CORE0.MAC_INTR_MAP); return @ptrFromInt(base + @sizeOf(u32) * @as(usize, @intFromEnum(source))); } + + pub const Status = struct { + reg: u61, + + pub fn init() Status { + return .{ + .reg = INTERRUPT_CORE0.INTR_STATUS_REG_0.raw | + (@as(u61, INTERRUPT_CORE0.INTR_STATUS_REG_1.raw) << 32), + }; + } + + pub fn is_set(status: Status, source: Source) bool { + return status.reg & (@as(u61, 1) << @intFromEnum(source)) != 0; + } + }; + + pub fn expect_handler(comptime int: Interrupt, comptime expected_handler: InterruptHandler) void { + const actual_handler = @field(microzig.options.interrupts, @tagName(int)); + if (!std.meta.eql(actual_handler, expected_handler)) + @compileError(std.fmt.comptimePrint("interrupt {t} not set to the expected handler", .{int})); + } }; pub const nop = riscv32_common.nop; pub const wfi = riscv32_common.wfi; +pub const fence = riscv32_common.fence; pub const startup_logic = struct { extern fn microzig_main() noreturn; @@ -320,6 +371,10 @@ fn init_interrupts() void { .mode = .vectored, .base = @intCast(@intFromPtr(&_vector_table) >> 2), }); + + if (interrupt_stack_options.enable) { + csr.mscratch.write_raw(0); + } } pub const TrapFrame = extern struct { @@ -339,314 +394,122 @@ pub const TrapFrame = extern struct { a5: usize, a6: usize, a7: usize, - s0: usize, - s1: usize, - s2: usize, - s3: usize, - s4: usize, - s5: usize, - s6: usize, - s7: usize, - s8: usize, - s9: usize, - s10: usize, - s11: usize, - gp: usize, - tp: usize, - sp: usize, - pc: usize, - mstatus: usize, - mcause: usize, - mtval: usize, }; -fn _vector_table() align(256) linksection(".ram_text") callconv(.naked) void { - comptime { - // TODO: make a better default exception handler - @export( - microzig.options.interrupts.Exception orelse &unhandled, - .{ .name = "_exception_handler" }, - ); - - for (std.meta.fieldNames(Interrupt)) |field_name| { - @export( - @field(microzig.options.interrupts, field_name) orelse &unhandled, - .{ .name = std.fmt.comptimePrint("_{s}_handler", .{field_name}) }, - ); +/// Statically allocated interrupt stack of the requested size. Used when the +/// interrupt stack option is enabled. +pub var interrupt_stack: [std.mem.alignForward(usize, interrupt_stack_options.size, 16)]u8 align(16) linksection(".ram_vectors") = undefined; + +fn _vector_table() align(256) linksection(".ram_vectors") callconv(.naked) void { + const interrupt_jump_asm, const interrupt_c_stubs_asm = comptime blk: { + var interrupt_jump_asm: []const u8 = ""; + var interrupt_c_stubs_asm: []const u8 = ""; + + for (std.meta.fieldNames(InterruptOptions)) |field_name| { + const handler: InterruptHandler = @field(microzig.options.interrupts, field_name) orelse .{ .c = &unhandled }; + switch (handler) { + .naked => |naked_handler| { + @export(naked_handler, .{ + .name = std.fmt.comptimePrint("_{s}_handler_naked", .{field_name}), + }); + + interrupt_jump_asm = interrupt_jump_asm ++ std.fmt.comptimePrint( + \\.balign 4 + \\ j _{s}_handler_naked + \\ + , .{field_name}); + }, + .c => |c_handler| { + @export(c_handler, .{ + .name = std.fmt.comptimePrint("_{s}_handler_c", .{field_name}), + }); + + interrupt_jump_asm = interrupt_jump_asm ++ std.fmt.comptimePrint( + \\.balign 4 + \\ j _{s}_stub + \\ + , .{field_name}); + + interrupt_c_stubs_asm = interrupt_c_stubs_asm ++ std.fmt.comptimePrint( + \\_{[name]s}_stub: + \\ addi sp, sp, -16*4 + \\ sw ra, 0(sp) + \\ la ra, _{[name]s}_handler_c + \\ j trap_common + \\ + , .{ .name = field_name }); + }, + } } - @export(&_update_priority, .{ .name = "_update_priority" }); - @export(&_restore_priority, .{ .name = "_restore_priority" }); - } + @export(&_handle_interrupt, .{ .name = "_handle_interrupt" }); - const interrupts_jump_asm = comptime blk: { - var s: []const u8 = &.{}; - for (1..32) |i| { - s = s ++ std.fmt.comptimePrint( - \\.balign 4 - \\ j interrupt{} - \\ - , .{i}); - } - break :blk s; + break :blk .{ interrupt_jump_asm, interrupt_c_stubs_asm }; }; - // adapted from https://github.com/esp-rs/esp-hal/blob/main/esp-riscv-rt/src/lib.rs - asm volatile ( - \\ j exception - \\ - ++ interrupts_jump_asm ++ - \\exception: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _exception_handler - \\ j trap_common - \\interrupt1: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt1_handler - \\ j trap_common - \\interrupt2: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt2_handler - \\ j trap_common - \\interrupt3: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt3_handler - \\ j trap_common - \\interrupt4: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt4_handler - \\ j trap_common - \\interrupt5: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt5_handler - \\ j trap_common - \\interrupt6: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt6_handler - \\ j trap_common - \\interrupt7: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt7_handler - \\ j trap_common - \\interrupt8: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt8_handler - \\ j trap_common - \\interrupt9: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt9_handler - \\ j trap_common - \\interrupt10: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt10_handler - \\ j trap_common - \\interrupt11: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt11_handler - \\ j trap_common - \\interrupt12: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt12_handler - \\ j trap_common - \\interrupt13: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt13_handler - \\ j trap_common - \\interrupt14: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt14_handler - \\ j trap_common - \\interrupt15: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt15_handler - \\ j trap_common - \\interrupt16: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt16_handler - \\ j trap_common - \\interrupt17: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt17_handler - \\ j trap_common - \\interrupt18: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt18_handler - \\ j trap_common - \\interrupt19: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt19_handler - \\ j trap_common - \\interrupt20: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt20_handler - \\ j trap_common - \\interrupt21: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt21_handler - \\ j trap_common - \\interrupt22: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt22_handler - \\ j trap_common - \\interrupt23: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt23_handler - \\ j trap_common - \\interrupt24: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt24_handler - \\ j trap_common - \\interrupt25: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt25_handler - \\ j trap_common - \\interrupt26: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt26_handler - \\ j trap_common - \\interrupt27: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt27_handler - \\ j trap_common - \\interrupt28: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt28_handler - \\ j trap_common - \\interrupt29: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt29_handler - \\ j trap_common - \\interrupt30: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt30_handler - \\ j trap_common - \\interrupt31: - \\ addi sp, sp, -35*4 - \\ sw ra, 0(sp) - \\ la ra, _interrupt31_handler - \\ j trap_common - \\trap_common: - \\ sw t0, 1*4(sp) - \\ sw t1, 2*4(sp) - \\ sw t2, 3*4(sp) - \\ sw t3, 4*4(sp) - \\ sw t4, 5*4(sp) - \\ sw t5, 6*4(sp) - \\ sw t6, 7*4(sp) - \\ sw a0, 8*4(sp) - \\ sw a1, 9*4(sp) - \\ sw a2, 10*4(sp) - \\ sw a3, 11*4(sp) - \\ sw a4, 12*4(sp) - \\ sw a5, 13*4(sp) - \\ sw a6, 14*4(sp) - \\ sw a7, 15*4(sp) - \\ sw s0, 16*4(sp) - \\ sw s1, 17*4(sp) - \\ sw s2, 18*4(sp) - \\ sw s3, 19*4(sp) - \\ sw s4, 20*4(sp) - \\ sw s5, 21*4(sp) - \\ sw s6, 22*4(sp) - \\ sw s7, 23*4(sp) - \\ sw s8, 24*4(sp) - \\ sw s9, 25*4(sp) - \\ sw s10, 26*4(sp) - \\ sw s11, 27*4(sp) - \\ sw gp, 28*4(sp) - \\ sw tp, 29*4(sp) - \\ addi t1, sp, 35*4 # what sp was on entry to trap - \\ sw t1, 30*4(sp) - \\ csrrs t1, mepc, x0 - \\ sw t1, 31*4(sp) - \\ csrrs t1, mstatus, x0 - \\ sw t1, 32*4(sp) - \\ csrrs t1, mcause, x0 - \\ sw t1, 33*4(sp) - \\ csrrs t1, mtval, x0 - \\ sw t1, 34*4(sp) - \\ - \\ mv s0, ra # Save address of handler function - \\ - \\ jal ra, _update_priority - \\ mv s1, a0 # Save the return of _update_priority - \\ - \\ mv a0, sp # Pass a pointer to TrapFrame to the handler function - \\ jalr ra, s0 # Call the handler function - \\ - \\ mv a0, s1 # Pass stored priority to _restore_priority - \\ jal ra, _restore_priority - \\ - \\ lw t1, 31*4(sp) - \\ csrrw x0, mepc, t1 - \\ - \\ lw t1, 32*4(sp) - \\ csrrw x0, mstatus, t1 - \\ - \\ lw ra, 0*4(sp) - \\ lw t0, 1*4(sp) - \\ lw t1, 2*4(sp) - \\ lw t2, 3*4(sp) - \\ lw t3, 4*4(sp) - \\ lw t4, 5*4(sp) - \\ lw t5, 6*4(sp) - \\ lw t6, 7*4(sp) - \\ lw a0, 8*4(sp) - \\ lw a1, 9*4(sp) - \\ lw a2, 10*4(sp) - \\ lw a3, 11*4(sp) - \\ lw a4, 12*4(sp) - \\ lw a5, 13*4(sp) - \\ lw a6, 14*4(sp) - \\ lw a7, 15*4(sp) - \\ lw s0, 16*4(sp) - \\ lw s1, 17*4(sp) - \\ lw s2, 18*4(sp) - \\ lw s3, 19*4(sp) - \\ lw s4, 20*4(sp) - \\ lw s5, 21*4(sp) - \\ lw s6, 22*4(sp) - \\ lw s7, 23*4(sp) - \\ lw s8, 24*4(sp) - \\ lw s9, 25*4(sp) - \\ lw s10, 26*4(sp) - \\ lw s11, 27*4(sp) - \\ lw gp, 28*4(sp) - \\ lw tp, 29*4(sp) - \\ lw sp, 30*4(sp) # This removes the frame we allocated from the stack - \\ - \\ mret + asm volatile (interrupt_jump_asm ++ interrupt_c_stubs_asm ++ + \\trap_common: + \\ sw t0, 1*4(sp) + \\ sw t1, 2*4(sp) + \\ sw t2, 3*4(sp) + \\ sw t3, 4*4(sp) + \\ sw t4, 5*4(sp) + \\ sw t5, 6*4(sp) + \\ sw t6, 7*4(sp) + \\ sw a0, 8*4(sp) + \\ sw a1, 9*4(sp) + \\ sw a2, 10*4(sp) + \\ sw a3, 11*4(sp) + \\ sw a4, 12*4(sp) + \\ sw a5, 13*4(sp) + \\ sw a6, 14*4(sp) + \\ sw a7, 15*4(sp) + \\ + \\ mv a0, sp # Pass a pointer to TrapFrame to the handler function + \\ mv a1, ra # Save address of handler function + \\ + ++ (if (interrupt_stack_options.enable) + // switch to interrupt stack if not nested + \\ csrr t0, mscratch + \\ bnez t0, 1f + \\ csrw mscratch, sp + \\ la sp, %[interrupt_stack_top] + \\ jal ra, _handle_interrupt + \\ csrrw sp, mscratch, x0 + \\ j 2f + \\1: + \\ jal ra, _handle_interrupt + \\2: + \\ + else + \\ jal ra, _handle_interrupt + \\ + ) ++ + \\ + \\ lw ra, 0*4(sp) + \\ lw t0, 1*4(sp) + \\ lw t1, 2*4(sp) + \\ lw t2, 3*4(sp) + \\ lw t3, 4*4(sp) + \\ lw t4, 5*4(sp) + \\ lw t5, 6*4(sp) + \\ lw t6, 7*4(sp) + \\ lw a0, 8*4(sp) + \\ lw a1, 9*4(sp) + \\ lw a2, 10*4(sp) + \\ lw a3, 11*4(sp) + \\ lw a4, 12*4(sp) + \\ lw a5, 13*4(sp) + \\ lw a6, 14*4(sp) + \\ lw a7, 15*4(sp) + \\ + \\ addi sp, sp, 16*4 # This removes the frame we allocated from the stack + \\ + \\ mret + : + : [interrupt_stack_top] "i" (if (interrupt_stack_options.enable) + interrupt_stack[interrupt_stack.len..].ptr + else {}), ); } @@ -657,36 +520,58 @@ fn unhandled(_: *TrapFrame) linksection(".ram_text") callconv(.c) void { std.log.err("unhandled interrupt {} occurred!", .{mcause.code}); } else { const exception: Exception = @enumFromInt(mcause.code); - std.log.err("exception {s} occurred!", .{@tagName(exception)}); + std.log.err("unhandled exception {s} occurred at {x}!", .{ @tagName(exception), csr.mepc.read_raw() }); + + switch (exception) { + .InstructionFault => std.log.err("faulting address: {x}", .{csr.mtval.read_raw()}), + .LoadFault => std.log.err("faulting address: {x}", .{csr.mtval.read_raw()}), + else => {}, + } } @panic("unhandled trap"); } -fn _update_priority() linksection(".ram_text") callconv(.c) u32 { +fn _handle_interrupt( + trap_frame: *TrapFrame, + handler: *const fn (*TrapFrame) callconv(.c) void, +) linksection(".ram_text") callconv(.c) void { const mcause = csr.mcause.read(); - const prev_priority = interrupt.get_priority_threshold(); - if (mcause.is_interrupt != 0) { - // this is an interrupt (can also be exception in which case we don't enable interrupts). + // interrupt const int: Interrupt = @enumFromInt(mcause.code); const priority = interrupt.get_priority(int); + // low priority interrupts can be preempted by higher priority interrupts if (@intFromEnum(priority) < 15) { - // allow higher priority interrupts to preempt this one + const mepc = csr.mepc.read_raw(); + const mstatus = csr.mstatus.read_raw(); + const mtval = csr.mtval.read_raw(); + + const prev_thresh = interrupt.get_priority_threshold(); interrupt.set_priority_threshold(@enumFromInt(@intFromEnum(priority) + 1)); + interrupt.enable_interrupts(); - } - } - return @intFromEnum(prev_priority); -} + handler(trap_frame); + + interrupt.disable_interrupts(); -fn _restore_priority(priority_raw: u32) linksection(".ram_text") callconv(.c) void { - interrupt.disable_interrupts(); - interrupt.set_priority_threshold(@enumFromInt(priority_raw)); + interrupt.set_priority_threshold(prev_thresh); + + csr.mepc.write_raw(mepc); + csr.mstatus.write_raw(mstatus); + csr.mtval.write_raw(mtval); + csr.mcause.write(mcause); + } else { + handler(trap_frame); + } + } else { + // exception + handler(trap_frame); + } } pub const csr = struct { diff --git a/port/espressif/esp/src/hal.zig b/port/espressif/esp/src/hal.zig index 95ff0a3ee..da6b6e679 100644 --- a/port/espressif/esp/src/hal.zig +++ b/port/espressif/esp/src/hal.zig @@ -1,18 +1,20 @@ const std = @import("std"); -const microzig = @import("microzig"); pub const esp_image = @import("esp_image"); +const microzig = @import("microzig"); pub const cache = @import("hal/cache.zig"); pub const clocks = @import("hal/clocks.zig"); pub const compatibility = @import("hal/compatibility.zig"); pub const drivers = @import("hal/drivers.zig"); -pub const gpio = @import("hal/gpio.zig"); pub const efuse = @import("hal/efuse.zig"); +pub const gpio = @import("hal/gpio.zig"); pub const i2c = @import("hal/i2c.zig"); pub const ledc = @import("hal/ledc.zig"); +pub const radio = @import("hal/radio.zig"); pub const rng = @import("hal/rng.zig"); pub const rom = @import("hal/rom.zig"); +pub const rtos = @import("hal/rtos.zig"); pub const spi = @import("hal/spi.zig"); pub const system = @import("hal/system.zig"); pub const systimer = @import("hal/systimer.zig"); @@ -23,8 +25,6 @@ pub const usb_serial_jtag = @import("hal/usb_serial_jtag.zig"); comptime { // export atomic intrinsics _ = @import("hal/atomic.zig"); - - _ = esp_app_desc; } pub const HAL_Options = struct { @@ -33,6 +33,8 @@ pub const HAL_Options = struct { secure_version: u32 = 0, version: []const u8 = "0.0.0", } = .{}, + rtos: rtos.Options = .{}, + radio: radio.Options = .{}, }; /// Clock config applied by the default `init()` function of the hal. @@ -55,6 +57,9 @@ pub fn init_sequence(clock_cfg: clocks.Config) void { system.init(); time.init(); + + if (microzig.options.hal.rtos.enable) + rtos.init(); } // NOTE: might be esp32c3 specific only + temporary until timers hal. diff --git a/port/espressif/esp/src/hal/radio.zig b/port/espressif/esp/src/hal/radio.zig new file mode 100644 index 000000000..6540fbb9f --- /dev/null +++ b/port/espressif/esp/src/hal/radio.zig @@ -0,0 +1,168 @@ +/// ESP Wi-Fi drivers integration. +/// +/// Based on https://github.com/esp-rs/esp-hal/tree/main/esp-radio. +const builtin = @import("builtin"); +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const c = @import("esp-wifi-driver"); +const microzig = @import("microzig"); +const TrapFrame = microzig.cpu.TrapFrame; +const peripherals = microzig.chip.peripherals; +const RTC_CNTL = peripherals.RTC_CNTL; +const APB_CTRL = peripherals.APB_CTRL; + +const efuse = @import("efuse.zig"); +pub const bluetooth = @import("radio/bluetooth.zig"); +const osi = @import("radio/osi.zig"); +const timer = @import("radio/timer.zig"); +pub const wifi = @import("radio/wifi.zig"); + +const log = std.log.scoped(.esp_radio); + +pub const Options = struct { + interrupt: microzig.cpu.Interrupt = .interrupt29, + wifi: wifi.Options = .{}, +}; + +var refcount: std.atomic.Value(usize) = .init(0); + +pub fn init(gpa: Allocator) Allocator.Error!void { + // TODO: check that clock frequency is higher or equal to 80mhz + + const radio_interrupt = microzig.options.hal.radio.interrupt; + + comptime { + if (!microzig.options.hal.rtos.enable) + @compileError("radio requires the rtos option to be enabled"); + + microzig.cpu.interrupt.expect_handler(radio_interrupt, interrupt_handler); + + osi.export_symbols(); + } + + if (refcount.rmw(.Add, 1, .monotonic) > 0) { + return; + } + + try timer.init(gpa); + + { + const cs = microzig.interrupt.enter_critical_section(); + defer cs.leave(); + + enable_wifi_power_domain_and_init_clocks(); + // phy_mem_init(); // only sets some global variable on esp32c3 + + osi.gpa = gpa; + + microzig.cpu.interrupt.map(.wifi_mac, radio_interrupt); + microzig.cpu.interrupt.map(.wifi_pwr, radio_interrupt); + microzig.cpu.interrupt.set_type(radio_interrupt, .level); + microzig.cpu.interrupt.set_priority(radio_interrupt, .highest); + } + + log.debug("initialization complete", .{}); + + const internal_wifi_log_level = switch (builtin.mode) { + .Debug => c.WIFI_LOG_VERBOSE, + else => c.WIFI_LOG_NONE, + }; + wifi.c_err(c.esp_wifi_internal_set_log_level(internal_wifi_log_level)) catch { + log.warn("failed to set wifi internal log level", .{}); + }; +} + +pub fn deinit() void { + const prev_count = refcount.rmw(.Sub, 1, .monotonic); + assert(prev_count != 0); + if (prev_count != 1) { + return; + } + + timer.deinit(); +} + +pub fn read_mac(iface: enum { + sta, + ap, + bt, +}) [6]u8 { + var mac = efuse.read_mac(); + switch (iface) { + .sta => {}, + .ap => mac[5] += 1, + .bt => mac[5] += 2, + } + return mac; +} + +fn enable_wifi_power_domain_and_init_clocks() void { + const system_wifibb_rst: u32 = 1 << 0; + const system_fe_rst: u32 = 1 << 1; + const system_wifimac_rst: u32 = 1 << 2; + const system_btbb_rst: u32 = 1 << 3; // bluetooth baseband + const system_btmac_rst: u32 = 1 << 4; // deprecated + const system_rw_btmac_rst: u32 = 1 << 9; // bluetooth mac + const system_rw_btmac_reg_rst: u32 = 1 << 11; // bluetooth mac registers + const system_btbb_reg_rst: u32 = 1 << 13; // bluetooth baseband registers + + const modem_reset_field_when_pu: u32 = system_wifibb_rst | + system_fe_rst | + system_wifimac_rst | + system_btbb_rst | + system_btmac_rst | + system_rw_btmac_rst | + system_rw_btmac_reg_rst | + system_btbb_reg_rst; + + RTC_CNTL.DIG_PWC.modify(.{ + .WIFI_FORCE_PD = 0, + .BT_FORCE_PD = 0, + }); + + APB_CTRL.WIFI_RST_EN.write(.{ .WIFI_RST = APB_CTRL.WIFI_RST_EN.read().WIFI_RST | modem_reset_field_when_pu }); + APB_CTRL.WIFI_RST_EN.write(.{ .WIFI_RST = APB_CTRL.WIFI_RST_EN.read().WIFI_RST & ~modem_reset_field_when_pu }); + + RTC_CNTL.DIG_ISO.modify(.{ + .WIFI_FORCE_ISO = 0, + .BT_FORCE_ISO = 0, + }); + + const system_wifi_clk_i2c_clk_en: u32 = 1 << 5; + const system_wifi_clk_unused_bit12: u32 = 1 << 12; + const wifi_bt_sdio_clk: u32 = system_wifi_clk_i2c_clk_en | system_wifi_clk_unused_bit12; + const system_wifi_clk_en: u32 = 0x00FB9FCF; + + RTC_CNTL.DIG_ISO.modify(.{ + .WIFI_FORCE_ISO = 0, + .BT_FORCE_ISO = 0, + }); + + RTC_CNTL.DIG_PWC.modify(.{ + .WIFI_FORCE_PD = 0, + .BT_FORCE_PD = 0, + }); + + APB_CTRL.WIFI_CLK_EN.write(.{ + .WIFI_CLK_EN = APB_CTRL.WIFI_CLK_EN.read().WIFI_CLK_EN & + ~wifi_bt_sdio_clk | + system_wifi_clk_en, + }); +} + +pub const interrupt_handler: microzig.cpu.InterruptHandler = .{ + .c = struct { + fn handler_fn(_: *TrapFrame) linksection(".ram_text") callconv(.c) void { + const status: microzig.cpu.interrupt.Status = .init(); + if (status.is_set(.wifi_mac) or status.is_set(.wifi_pwr)) { + if (osi.wifi_interrupt_handler) |handler| { + handler.f(handler.arg); + } else { + // should be unreachable + } + } + } + }.handler_fn, +}; diff --git a/port/espressif/esp/src/hal/radio/bluetooth.zig b/port/espressif/esp/src/hal/radio/bluetooth.zig new file mode 100644 index 000000000..e69de29bb diff --git a/port/espressif/esp/src/hal/radio/libc_dummy_include/assert.h b/port/espressif/esp/src/hal/radio/libc_dummy_include/assert.h new file mode 100644 index 000000000..e69de29bb diff --git a/port/espressif/esp/src/hal/radio/libc_dummy_include/stdio.h b/port/espressif/esp/src/hal/radio/libc_dummy_include/stdio.h new file mode 100644 index 000000000..c5aea2c49 --- /dev/null +++ b/port/espressif/esp/src/hal/radio/libc_dummy_include/stdio.h @@ -0,0 +1,5 @@ +#include + +typedef uint32_t size_t; + +typedef void FILE; diff --git a/port/espressif/esp/src/hal/radio/libc_dummy_include/stdlib.h b/port/espressif/esp/src/hal/radio/libc_dummy_include/stdlib.h new file mode 100644 index 000000000..e69de29bb diff --git a/port/espressif/esp/src/hal/radio/libc_dummy_include/sys/lock.h b/port/espressif/esp/src/hal/radio/libc_dummy_include/sys/lock.h new file mode 100644 index 000000000..e69de29bb diff --git a/port/espressif/esp/src/hal/radio/libc_dummy_include/sys/queue.h b/port/espressif/esp/src/hal/radio/libc_dummy_include/sys/queue.h new file mode 100644 index 000000000..e69de29bb diff --git a/port/espressif/esp/src/hal/radio/osi.zig b/port/espressif/esp/src/hal/radio/osi.zig new file mode 100644 index 000000000..8ad674170 --- /dev/null +++ b/port/espressif/esp/src/hal/radio/osi.zig @@ -0,0 +1,1275 @@ +const std = @import("std"); +const assert = std.debug.assert; +const builtin = @import("builtin"); + +const c = @import("esp-wifi-driver"); +const microzig = @import("microzig"); +const enter_critical_section = microzig.interrupt.enter_critical_section; +const time = microzig.drivers.time; +const peripherals = microzig.chip.peripherals; +const APB_CTRL = peripherals.APB_CTRL; +const radio_interrupt = microzig.options.hal.radio.interrupt; + +const radio = @import("../radio.zig"); +const rng = @import("../rng.zig"); +const rtos = @import("../rtos.zig"); +const systimer = @import("../systimer.zig"); +const get_time_since_boot = @import("../time.zig").get_time_since_boot; +const timer = @import("timer.zig"); +const wifi = @import("wifi.zig"); + +const log = std.log.scoped(.esp_radio_osi); + +// TODO: config +const coex_enabled: bool = false; + +pub var gpa: std.mem.Allocator = undefined; + +pub var wifi_interrupt_handler: ?struct { + f: *const fn (?*anyopaque) callconv(.c) void, + arg: ?*anyopaque, +} = undefined; + +extern fn vsnprintf(buffer: [*c]u8, len: usize, fmt: [*c]const u8, va_list: std.builtin.VaList) callconv(.c) void; + +fn syslog(fmt: ?[*:0]const u8, va_list: std.builtin.VaList) callconv(.c) void { + var buf: [512:0]u8 = undefined; + vsnprintf(&buf, 512, fmt, va_list); + log.debug("{s}", .{std.mem.span((&buf).ptr)}); +} + +// ----- exports ----- + +pub fn strlen(str: ?[*:0]const u8) callconv(.c) usize { + const s = str orelse return 0; + + return std.mem.len(s); +} + +pub fn strnlen(str: ?[*:0]const u8, _: usize) callconv(.c) usize { + // const s = str orelse return 0; + // return if (std.mem.indexOfScalar(u8, s[0..n], 0)) |index| index + 1 else n; + const s = str orelse return 0; + + return std.mem.len(s); +} + +pub fn strrchr(str: ?[*:0]const u8, chr: u32) callconv(.c) ?[*:0]const u8 { + const s = str orelse return null; + + // Should return even the index of the zero byte if requested. This + // implementation only works with single byte characters. + + if (std.mem.lastIndexOfScalar(u8, s[0 .. std.mem.len(s) + 1], @intCast(chr))) |index| { + return @ptrFromInt(@intFromPtr(s) + index); + } else { + return null; + } +} + +pub fn __assert_func( + file: ?[*:0]const u8, + line: u32, + func: ?[*:0]const u8, + failed_expr: ?[*:0]const u8, +) callconv(.c) void { + switch (builtin.mode) { + .Debug, .ReleaseSafe => { + log.err("assertion failed: `{s}` in file {s}, line {}, function {s}", .{ + failed_expr orelse "", + file orelse "", + line, + func orelse "", + }); + @panic("assertion failed"); + }, + .ReleaseSmall => @panic("assertion failed"), + .ReleaseFast => unreachable, + } +} + +pub fn malloc(len: usize) callconv(.c) ?*anyopaque { + log.debug("malloc {}", .{len}); + + const buf = gpa.rawAlloc(8 + len, .@"8", @returnAddress()) orelse { + log.warn("failed to allocate memory: {}", .{len}); + return null; + }; + + const alloc_len: *usize = @ptrCast(@alignCast(buf)); + alloc_len.* = len; + return @ptrFromInt(@intFromPtr(buf) + 8); +} + +pub fn calloc(number: usize, size: usize) callconv(.c) ?*anyopaque { + const total_size: usize = number * size; + if (malloc(total_size)) |ptr| { + @memset(@as([*]u8, @ptrCast(ptr))[0..total_size], 0); + return ptr; + } + return null; +} + +pub fn free(ptr: ?*anyopaque) callconv(.c) void { + log.debug("free {?}", .{ptr}); + + if (ptr == null) { + log.warn("ignoring free(null) called by 0x{x}", .{@returnAddress()}); + return; + } + + const buf_ptr: [*]u8 = @ptrFromInt(@intFromPtr(ptr) - 8); + const buf_len: *usize = @ptrCast(@alignCast(buf_ptr)); + gpa.rawFree(buf_ptr[0 .. @sizeOf(usize) + buf_len.*], .@"8", @returnAddress()); +} + +pub fn puts(ptr: ?*anyopaque) callconv(.c) void { + const s: []const u8 = std.mem.span(@as([*:0]const u8, @ptrCast(ptr))); + log.debug("{s}", .{s}); +} + +pub fn gettimeofday(tv: ?*c.timeval, _: ?*anyopaque) callconv(.c) i32 { + if (tv) |time_val| { + const usec = get_time_since_boot().to_us(); + time_val.tv_sec = usec / 1_000_000; + time_val.tv_usec = @intCast(usec % 1_000_000); + } + + return 0; +} + +pub fn sleep(time_sec: c_uint) callconv(.c) c_int { + rtos.sleep(.from_us(time_sec * 1_000_000)); + return 0; +} + +pub fn usleep(time_us: u32) callconv(.c) c_int { + rtos.sleep(.from_us(time_us)); + return 0; +} + +pub fn vTaskDelay(ticks: u32) callconv(.c) void { + rtos.sleep(.from_us(ticks)); +} + +pub var WIFI_EVENT: c.esp_event_base_t = "WIFI_EVENT"; + +pub fn printf(fmt: ?[*:0]const u8, ...) callconv(.c) void { + syslog(fmt, @cVaStart()); +} + +pub fn esp_fill_random(buf: [*c]u8, len: usize) callconv(.c) void { + log.debug("esp_fill_random {any} {}", .{ buf, len }); + + rng.read(buf[0..len]); +} + +/// Some are weaklinks which can be overriten. +pub fn export_symbols() void { + @export(&strlen, .{ .name = "strlen", .linkage = .weak }); + @export(&strnlen, .{ .name = "strnlen", .linkage = .weak }); + @export(&strrchr, .{ .name = "strrchr", .linkage = .weak }); + + @export(&__assert_func, .{ .name = "__assert_func", .linkage = .weak }); + + @export(&malloc, .{ .name = "malloc", .linkage = .weak }); + @export(&calloc, .{ .name = "calloc", .linkage = .weak }); + @export(&free, .{ .name = "free", .linkage = .weak }); + @export(&puts, .{ .name = "puts", .linkage = .weak }); + + @export(&gettimeofday, .{ .name = "gettimeofday", .linkage = .weak }); + @export(&sleep, .{ .name = "sleep", .linkage = .weak }); + @export(&usleep, .{ .name = "usleep", .linkage = .weak }); + @export(&vTaskDelay, .{ .name = "vTaskDelay", .linkage = .weak }); + + @export(&WIFI_EVENT, .{ .name = "WIFI_EVENT" }); + inline for (&.{ + "rtc_printf", + "phy_printf", + "coexist_printf", + "net80211_printf", + "pp_printf", + }) |name| { + @export(&printf, .{ .name = name }); + } + @export(&esp_fill_random, .{ .name = "esp_fill_random" }); + @export(&esp_event_post, .{ .name = "esp_event_post" }); +} + +// ----- end of exports ----- + +pub fn env_is_chip() callconv(.c) bool { + return true; +} + +pub fn set_intr(cpu_no: i32, intr_source: u32, intr_num: u32, intr_prio: i32) callconv(.c) void { + log.debug("set_intr {} {} {} {}", .{ cpu_no, intr_source, intr_num, intr_prio }); + + // esp32c3 + // these are already set up + // possible calls: + // INFO - set_intr 0 2 1 1 (WIFI_PWR) + // INFO - set_intr 0 0 1 1 (WIFI_MAC) + // + // we don't need to do anything as these interrupts are already setup in + // `radio.init` +} + +pub fn clear_intr(intr_source: u32, intr_num: u32) callconv(.c) void { + log.debug("clear_intr {} {}", .{ intr_source, intr_num }); +} + +pub fn set_isr( + n: i32, + f: ?*anyopaque, + arg: ?*anyopaque, +) callconv(.c) void { + log.debug("set_isr {} {?} {?}", .{ n, f, arg }); + + // esp32c3 specific + + switch (n) { + 0, 1 => { + wifi_interrupt_handler = if (f) |handler| .{ + .f = @ptrCast(@alignCast(handler)), + .arg = arg, + } else null; + }, + else => @panic("invalid interrupt number"), + } +} + +pub fn ints_on(mask: u32) callconv(.c) void { + log.debug("ints_on {}", .{mask}); + + if (mask == 2) { + microzig.cpu.interrupt.enable(radio_interrupt); + } else { + @panic("ints_on: not implemented"); + } +} + +pub fn ints_off(mask: u32) callconv(.c) void { + log.debug("ints_off {}", .{mask}); + + if (mask == 2) { + microzig.cpu.interrupt.disable(radio_interrupt); + } else { + @panic("ints_off: not implemented"); + } +} + +pub fn is_from_isr() callconv(.c) bool { + return true; +} + +pub fn spin_lock_create() callconv(.c) ?*anyopaque { + log.debug("spin_lock_create", .{}); + return semphr_create(1, 1); +} + +pub fn spin_lock_delete(ptr: ?*anyopaque) callconv(.c) void { + log.debug("spin_lock_delete {?}", .{ptr}); + return semphr_delete(ptr); +} + +pub fn wifi_int_disable(mux_ptr: ?*anyopaque) callconv(.c) u32 { + log.debug("wifi_int_disable {?}", .{mux_ptr}); + + const ints_enabled = microzig.cpu.interrupt.globally_enabled(); + if (ints_enabled) { + microzig.cpu.interrupt.disable_interrupts(); + } + return @intFromBool(ints_enabled); +} + +pub fn wifi_int_restore(mux_ptr: ?*anyopaque, state: u32) callconv(.c) void { + log.debug("wifi_int_restore {?} {}", .{ mux_ptr, state }); + + // check if interrupts were enabled before. + if (state != 0) { + microzig.cpu.interrupt.enable_interrupts(); + } +} + +pub fn task_yield_from_isr() callconv(.c) void { + log.debug("task_yield_from_isr", .{}); + + rtos.yield_from_isr(); +} + +pub fn semphr_create(max_value: u32, init_value: u32) callconv(.c) ?*anyopaque { + log.debug("semphr_create {} {}", .{ max_value, init_value }); + + const sem = gpa.create(rtos.Semaphore) catch { + log.warn("failed to allocate semaphore", .{}); + return null; + }; + sem.* = .init(init_value, max_value); + + log.debug(">>>> semaphore create: {*}", .{sem}); + + return sem; +} + +pub fn semphr_delete(ptr: ?*anyopaque) callconv(.c) void { + log.debug("semphr_delete {?}", .{ptr}); + + gpa.destroy(@as(*rtos.Semaphore, @ptrCast(@alignCast(ptr)))); +} + +pub fn semphr_take(ptr: ?*anyopaque, tick: u32) callconv(.c) i32 { + log.debug("semphr_take {?} {}", .{ ptr, tick }); + + const sem: *rtos.Semaphore = @ptrCast(@alignCast(ptr)); + const maybe_timeout: ?time.Duration = if (tick == c.OSI_FUNCS_TIME_BLOCKING) + .from_us(tick) + else + null; + sem.take_with_timeout(maybe_timeout) catch { + log.debug(">>>> return from semaphore take with timeout: {*}", .{sem}); + return 1; + }; + + return 0; +} + +pub fn semphr_give(ptr: ?*anyopaque) callconv(.c) i32 { + log.debug("semphr_give {?}", .{ptr}); + + const sem: *rtos.Semaphore = @ptrCast(@alignCast(ptr)); + sem.give(); + return 1; +} + +pub fn wifi_thread_semphr_get() callconv(.c) ?*anyopaque { + return &rtos.get_current_task().semaphore; +} + +const RecursiveMutex = struct { + recursive: bool, + value: u32 = 0, + owning_task: ?*rtos.Task = null, + prev_priority: ?rtos.Priority = null, + wait_queue: rtos.PriorityWaitQueue = .{}, + + pub fn lock(mutex: *RecursiveMutex) void { + const cs = enter_critical_section(); + defer cs.leave(); + + const current_task = rtos.get_current_task(); + + if (mutex.owning_task == current_task) { + assert(mutex.recursive); + mutex.value += 1; + return; + } + + while (mutex.owning_task) |owning_task| { + // Owning task inherits the priority of the current task if it the + // current task has a bigger priority. + if (@intFromEnum(current_task.priority) > @intFromEnum(owning_task.priority)) { + mutex.prev_priority = owning_task.priority; + owning_task.priority = current_task.priority; + rtos.make_ready(owning_task); + } + + mutex.wait_queue.wait(current_task, null); + } + + assert(mutex.value == 0); + mutex.value += 1; + mutex.owning_task = current_task; + } + + pub fn unlock(mutex: *RecursiveMutex) bool { + const cs = enter_critical_section(); + defer cs.leave(); + + assert(mutex.owning_task == rtos.get_current_task()); + + assert(mutex.value > 0); + mutex.value -= 1; + if (mutex.value <= 0) { + const owning_task = mutex.owning_task.?; + + // Restore the priority of the task + if (mutex.prev_priority) |prev_priority| { + owning_task.priority = prev_priority; + mutex.prev_priority = null; + } + mutex.owning_task = null; + mutex.wait_queue.wake_one(); + return true; + } else { + return false; + } + } +}; + +pub fn mutex_create() callconv(.c) ?*anyopaque { + log.debug("mutex_create", .{}); + + const mutex = gpa.create(RecursiveMutex) catch { + log.warn("failed to allocate recursive mutex", .{}); + return null; + }; + mutex.* = .{ .recursive = false }; + + log.debug(">>>> mutex create: {*}", .{mutex}); + + return mutex; +} + +pub fn recursive_mutex_create() callconv(.c) ?*anyopaque { + log.debug("recursive_mutex_create", .{}); + + const mutex = gpa.create(RecursiveMutex) catch { + log.warn("failed to allocate recursive mutex", .{}); + return null; + }; + mutex.* = .{ .recursive = true }; + + log.debug(">>>> mutex create: {*}", .{mutex}); + + return mutex; +} + +pub fn mutex_delete(ptr: ?*anyopaque) callconv(.c) void { + log.debug("mutex_delete {?}", .{ptr}); + + const mutex: *RecursiveMutex = @ptrCast(@alignCast(ptr)); + gpa.destroy(mutex); +} + +pub fn mutex_lock(ptr: ?*anyopaque) callconv(.c) i32 { + log.debug("mutex lock {?}", .{ptr}); + + const mutex: *RecursiveMutex = @ptrCast(@alignCast(ptr)); + mutex.lock(); + + return 1; +} + +pub fn mutex_unlock(ptr: ?*anyopaque) callconv(.c) i32 { + log.debug("mutex unlock {?}", .{ptr}); + + const mutex: *RecursiveMutex = @ptrCast(@alignCast(ptr)); + return @intFromBool(mutex.unlock()); +} + +pub const QueueWrapper = struct { + item_len: u32, + inner: rtos.TypeErasedQueue, +}; + +pub fn queue_create(capacity: u32, item_len: u32) callconv(.c) ?*anyopaque { + log.debug("queue_create {} {}", .{ capacity, item_len }); + + const new_cap, const new_item_len = if (capacity != 3 and item_len != 4) + .{ capacity, item_len } + else blk: { + log.warn("fixing queue item len", .{}); + break :blk .{ 3, 8 }; + }; + + const buf: []u8 = gpa.alloc(u8, new_cap * item_len) catch { + log.warn("failed to allocate queue buffer", .{}); + return null; + }; + + const queue = gpa.create(QueueWrapper) catch { + log.warn("failed to allocate queue", .{}); + gpa.free(buf); + return null; + }; + + queue.* = .{ + .item_len = new_item_len, + .inner = .init(buf), + }; + + log.debug(">>>> queue create: {*}", .{queue}); + + return queue; +} + +pub fn queue_delete(ptr: ?*anyopaque) callconv(.c) void { + log.debug("queue_delete {?}", .{ptr}); + + const queue: *QueueWrapper = @ptrCast(@alignCast(ptr)); + gpa.free(queue.inner.buffer); + gpa.destroy(queue); +} + +pub fn queue_send(ptr: ?*anyopaque, item_ptr: ?*anyopaque, block_time_tick: u32) callconv(.c) i32 { + log.debug("queue_send {?} {?} {}", .{ ptr, item_ptr, block_time_tick }); + + const queue: *QueueWrapper = @ptrCast(@alignCast(ptr)); + const item: [*]const u8 = @ptrCast(@alignCast(item_ptr)); + + const size = switch (block_time_tick) { + 0 => queue.inner.put_non_blocking(item[0..queue.item_len]), + else => queue.inner.put( + item[0..queue.item_len], + 1, + if (block_time_tick == c.OSI_FUNCS_TIME_BLOCKING) + .from_us(block_time_tick) + else + null, + ), + }; + if (size == 0) return -1; + return 1; +} + +pub fn queue_send_from_isr(ptr: ?*anyopaque, item_ptr: ?*anyopaque, _hptw: ?*anyopaque) callconv(.c) i32 { + log.debug("queue_send_from_isr {?} {?} {?}", .{ ptr, item_ptr, _hptw }); + + const queue: *QueueWrapper = @ptrCast(@alignCast(ptr)); + const item: [*]const u8 = @ptrCast(@alignCast(item_ptr)); + const n = @divExact(queue.inner.put_non_blocking(item[0..queue.item_len]), queue.item_len); + + @as(*u32, @ptrCast(@alignCast(_hptw))).* = @intFromBool(rtos.is_a_higher_priority_task_ready()); + + return @intCast(n); +} + +pub fn queue_send_to_back() callconv(.c) void { + @panic("queue_send_to_back: not implemented"); +} + +pub fn queue_send_to_front() callconv(.c) void { + @panic("queue_send_to_front: not implemented"); +} + +pub fn queue_recv(ptr: ?*anyopaque, item_ptr: ?*anyopaque, block_time_tick: u32) callconv(.c) i32 { + log.debug("queue_recv {?} {?} {}", .{ ptr, item_ptr, block_time_tick }); + + const queue: *QueueWrapper = @ptrCast(@alignCast(ptr)); + const item: [*]u8 = @ptrCast(@alignCast(item_ptr)); + + const size = switch (block_time_tick) { + 0 => queue.inner.get_non_blocking(item[0..queue.item_len]), + else => queue.inner.get( + item[0..queue.item_len], + queue.item_len, + if (block_time_tick == c.OSI_FUNCS_TIME_BLOCKING) + .from_us(block_time_tick) + else + null, + ), + }; + if (size == 0) return -1; + return 1; +} + +pub fn queue_msg_waiting(ptr: ?*anyopaque) callconv(.c) u32 { + log.debug("queue_msg_waiting {?}", .{ptr}); + + const queue: *QueueWrapper = @ptrCast(@alignCast(ptr)); + return @divExact(queue.inner.len, queue.item_len); +} + +pub fn event_group_create() callconv(.c) void { + @panic("event_group_create: not implemented"); +} + +pub fn event_group_delete() callconv(.c) void { + @panic("event_group_delete: not implemented"); +} + +pub fn event_group_set_bits() callconv(.c) void { + @panic("event_group_set_bits: not implemented"); +} + +pub fn event_group_clear_bits() callconv(.c) void { + @panic("event_group_clear_bits: not implemented"); +} + +pub fn event_group_wait_bits() callconv(.c) void { + @panic("event_group_wait_bits: not implemented"); +} + +fn task_wrapper( + task_entry: *const fn (param: ?*anyopaque) callconv(.c) noreturn, + param: ?*anyopaque, +) noreturn { + task_entry(param); +} + +fn task_create_common( + task_func: ?*anyopaque, + name: [*c]const u8, + stack_depth: u32, + param: ?*anyopaque, + prio: u32, + task_handle: ?*anyopaque, + core_id: u32, +) i32 { + _ = core_id; // autofix + + const task_entry: *const fn (param: ?*anyopaque) callconv(.c) noreturn = @ptrCast(@alignCast(task_func)); + + // increase stack size if we are in debug mode + const stack_size: usize = stack_depth + if (builtin.mode == .Debug) 6000 else 0; + const task: *rtos.Task = rtos.spawn(gpa, task_wrapper, .{ task_entry, param }, .{ + .name = std.mem.span(name), + .priority = @enumFromInt(prio), + .stack_size = stack_size, + }) catch { + log.warn("failed to create task", .{}); + return 0; + }; + + @as(*usize, @ptrCast(@alignCast(task_handle))).* = @intFromPtr(task); + + return 1; +} + +pub fn task_create_pinned_to_core( + task_func: ?*anyopaque, + name: [*c]const u8, + stack_depth: u32, + param: ?*anyopaque, + prio: u32, + task_handle: ?*anyopaque, + core_id: u32, +) callconv(.c) i32 { + log.debug("task_create_pinned_to_core {?} {s} {} {?} {} {?} {}", .{ + task_func, + name, + stack_depth, + param, + prio, + task_handle, + core_id, + }); + + return task_create_common(task_func, name, stack_depth, param, prio, task_handle, core_id); +} + +pub fn task_create( + task_func: ?*anyopaque, + name: [*c]const u8, + stack_depth: u32, + param: ?*anyopaque, + prio: u32, + task_handle: ?*anyopaque, +) callconv(.c) i32 { + log.debug("task_create {?} {s} {} {?} {} {?}", .{ + task_func, + name, + stack_depth, + param, + prio, + task_handle, + }); + + return task_create_common(task_func, name, stack_depth, param, prio, task_handle, 0); +} + +pub fn task_delete(handle: ?*anyopaque) callconv(.c) void { + log.debug("task_delete {?}", .{handle}); + if (handle != null) { + @panic("task_delete(non-null): not implemented"); + } + rtos.yield(.delete); +} + +pub fn task_delay(tick: u32) callconv(.c) void { + log.debug("task_delay {}", .{tick}); + + rtos.sleep(.from_us(tick)); +} + +pub fn task_ms_to_tick(ms: u32) callconv(.c) i32 { + return @intCast(ms * 1_000); +} + +pub fn task_get_current_task() callconv(.c) ?*anyopaque { + return rtos.get_current_task(); +} + +pub fn task_get_max_priority() callconv(.c) i32 { + return @intFromEnum(rtos.Priority.highest); +} + +pub fn esp_event_post( + base: [*c]const u8, + id: i32, + data: ?*anyopaque, + data_size: usize, + ticks_to_wait: u32, +) callconv(.c) i32 { + _ = base; + _ = ticks_to_wait; + wifi.on_event_post(id, data, data_size); + return 0; +} + +pub fn get_free_heap_size() callconv(.c) void { + @panic("get_free_heap_size: not implemented"); +} + +pub fn rand() callconv(.c) u32 { + return rng.random_u32(); +} + +pub fn dport_access_stall_other_cpu_start_wrap() callconv(.c) void { + log.debug("dport_access_stall_other_cpu_start_wrap", .{}); +} + +pub fn dport_access_stall_other_cpu_end_wrap() callconv(.c) void { + log.debug("dport_access_stall_other_cpu_end_wrap", .{}); +} + +pub fn wifi_apb80m_request() callconv(.c) void { + log.debug("wifi_apb80m_request", .{}); +} + +pub fn wifi_apb80m_release() callconv(.c) void { + log.debug("wifi_apb80m_release", .{}); +} + +const CONFIG_ESP32_PHY_MAX_TX_POWER: u8 = 20; + +fn limit(val: u8, low: u8, high: u8) u8 { + return if (val < low) low else if (val > high) high else val; +} + +const phy_init_data_default: c.esp_phy_init_data_t = .{ + .params = .{ + 0x00, + 0x00, + limit(CONFIG_ESP32_PHY_MAX_TX_POWER * 4, 0, 0x50), + limit(CONFIG_ESP32_PHY_MAX_TX_POWER * 4, 0, 0x50), + limit(CONFIG_ESP32_PHY_MAX_TX_POWER * 4, 0, 0x50), + limit(CONFIG_ESP32_PHY_MAX_TX_POWER * 4, 0, 0x4c), + limit(CONFIG_ESP32_PHY_MAX_TX_POWER * 4, 0, 0x4c), + limit(CONFIG_ESP32_PHY_MAX_TX_POWER * 4, 0, 0x48), + limit(CONFIG_ESP32_PHY_MAX_TX_POWER * 4, 0, 0x4c), + limit(CONFIG_ESP32_PHY_MAX_TX_POWER * 4, 0, 0x48), + limit(CONFIG_ESP32_PHY_MAX_TX_POWER * 4, 0, 0x48), + limit(CONFIG_ESP32_PHY_MAX_TX_POWER * 4, 0, 0x44), + limit(CONFIG_ESP32_PHY_MAX_TX_POWER * 4, 0, 0x4a), + limit(CONFIG_ESP32_PHY_MAX_TX_POWER * 4, 0, 0x46), + limit(CONFIG_ESP32_PHY_MAX_TX_POWER * 4, 0, 0x46), + limit(CONFIG_ESP32_PHY_MAX_TX_POWER * 4, 0, 0x42), + 0x00, + 0x00, + 0x00, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0x74, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + }, +}; + +fn clocks_phy_set_enabled(enable: bool) void { + const system_wifi_clk_wifi_bt_common_m: u32 = 0x78078F; + + var wifi_clk_en = APB_CTRL.WIFI_CLK_EN.read().WIFI_CLK_EN; + if (enable) { + wifi_clk_en |= system_wifi_clk_wifi_bt_common_m; + } else { + wifi_clk_en &= ~system_wifi_clk_wifi_bt_common_m; + } + APB_CTRL.WIFI_CLK_EN.write(.{ .WIFI_CLK_EN = wifi_clk_en }); +} + +pub fn phy_disable() callconv(.c) void { + log.debug("phy_disable", .{}); + clocks_phy_set_enabled(false); +} + +pub fn phy_enable() callconv(.c) void { + log.debug("phy_enable", .{}); + + const cs = enter_critical_section(); + defer cs.leave(); + + clocks_phy_set_enabled(true); + + // TODO: fully implement phy enable + var cal_data: c.esp_phy_calibration_data_t = std.mem.zeroes(c.esp_phy_calibration_data_t); + + const phy_version = c.get_phy_version_str(); + log.debug("phy_version {s}", .{phy_version}); + + _ = c.register_chipv7_phy(&phy_init_data_default, &cal_data, c.PHY_RF_CAL_PARTIAL); +} + +pub fn phy_update_country_info(country: [*c]const u8) callconv(.c) c_int { + log.debug("phy_update_country_info {s}", .{country}); + return -1; +} + +pub fn read_mac(mac: [*c]u8, typ: c_uint) callconv(.c) c_int { + log.debug("read_mac {*} {}", .{ mac, typ }); + + const mac_tmp: [6]u8 = radio.read_mac(switch (typ) { + 0 => .sta, + 1 => .ap, + 2 => .bt, + else => @panic("unknown mac typ"), + }); + @memcpy(mac[0..6], &mac_tmp); + + return 0; +} + +pub fn timer_arm(ets_timer_ptr: ?*anyopaque, ms: u32, repeat: bool) callconv(.c) void { + timer_arm_us(ets_timer_ptr, ms * 1_000, repeat); +} + +pub fn timer_disarm(ets_timer_ptr: ?*anyopaque) callconv(.c) void { + log.debug("timer_disarm {?}", .{ets_timer_ptr}); + + const ets_timer: *c.ets_timer = @ptrCast(@alignCast(ets_timer_ptr)); + timer.disarm(ets_timer); +} + +pub fn timer_done(ets_timer_ptr: ?*anyopaque) callconv(.c) void { + log.debug("timer_done {?}", .{ets_timer_ptr}); + + const ets_timer: *c.ets_timer = @ptrCast(@alignCast(ets_timer_ptr)); + timer.done(gpa, ets_timer); +} + +pub fn timer_setfn(ets_timer_ptr: ?*anyopaque, callback_ptr: ?*anyopaque, arg: ?*anyopaque) callconv(.c) void { + log.debug("timer_setfn {?} {?} {?}", .{ ets_timer_ptr, callback_ptr, arg }); + + const ets_timer: *c.ets_timer = @ptrCast(@alignCast(ets_timer_ptr)); + const callback: timer.CallbackFn = @ptrCast(@alignCast(callback_ptr)); + timer.setfn(gpa, ets_timer, callback, arg) catch { + log.warn("failed to allocate timer", .{}); + }; +} + +pub fn timer_arm_us(ets_timer_ptr: ?*anyopaque, us: u32, repeat: bool) callconv(.c) void { + log.debug("timer_arm_us {?} {} {}", .{ ets_timer_ptr, us, repeat }); + + const ets_timer: *c.ets_timer = @ptrCast(@alignCast(ets_timer_ptr)); + timer.arm(ets_timer, .from_us(us), repeat); +} + +pub fn wifi_reset_mac() callconv(.c) void { + log.debug("wifi_reset_mac", .{}); + + APB_CTRL.WIFI_RST_EN.write(.{ .WIFI_RST = APB_CTRL.WIFI_RST_EN.read().WIFI_RST | (1 << 2) }); + APB_CTRL.WIFI_RST_EN.write(.{ .WIFI_RST = APB_CTRL.WIFI_RST_EN.read().WIFI_RST & ~@as(u32, 1 << 2) }); +} + +pub fn wifi_clock_enable() callconv(.c) void { + log.debug("wifi_clock_enable", .{}); + + // no op on esp32c3 +} + +pub fn wifi_clock_disable() callconv(.c) void { + log.debug("wifi_clock_disable", .{}); + + // no op on esp32c3 +} + +pub fn wifi_rtc_enable_iso() callconv(.c) void { + @panic("wifi_rtc_enable_iso: not implemented"); +} + +pub fn wifi_rtc_disable_iso() callconv(.c) void { + @panic("wifi_rtc_disable_iso: not implemented"); +} + +pub fn esp_timer_get_time() callconv(.c) i64 { + log.debug("esp_timer_get_time", .{}); + + return @intCast(get_time_since_boot().to_us()); +} + +pub fn nvs_set_i8() callconv(.c) void { + @panic("nvs_set_i8: not implemented"); +} + +pub fn nvs_get_i8() callconv(.c) void { + @panic("nvs_get_i8: not implemented"); +} + +pub fn nvs_set_u8() callconv(.c) void { + @panic("nvs_set_u8: not implemented"); +} + +pub fn nvs_get_u8() callconv(.c) void { + @panic("nvs_get_u8: not implemented"); +} + +pub fn nvs_set_u16() callconv(.c) void { + @panic("nvs_set_u16: not implemented"); +} + +pub fn nvs_get_u16() callconv(.c) void { + @panic("nvs_get_u16: not implemented"); +} + +pub fn nvs_open() callconv(.c) void { + @panic("nvs_open: not implemented"); +} + +pub fn nvs_close() callconv(.c) void { + @panic("nvs_close: not implemented"); +} + +pub fn nvs_commit() callconv(.c) void { + @panic("nvs_commit: not implemented"); +} + +pub fn nvs_set_blob() callconv(.c) void { + @panic("nvs_set_blob: not implemented"); +} + +pub fn nvs_get_blob() callconv(.c) void { + @panic("nvs_get_blob: not implemented"); +} + +pub fn nvs_erase_key() callconv(.c) void { + @panic("nvs_erase_key: not implemented"); +} + +pub fn get_random(buf: [*c]u8, len: usize) callconv(.c) c_int { + rng.read(buf[0..len]); + return 0; +} + +pub fn get_time() callconv(.c) void { + @panic("get_time: not implemented"); +} + +pub fn random() callconv(.c) c_ulong { + return rng.random_u32(); +} + +pub fn slowclk_cal_get() callconv(.c) u32 { + // NOTE: esp32c3 specific + return 28639; +} + +pub fn log_write(_: c_uint, _: [*c]const u8, fmt: [*c]const u8, ...) callconv(.c) void { + syslog(fmt, @cVaStart()); +} + +pub fn log_writev(_: c_uint, _: [*c]const u8, fmt: [*c]const u8, va_list: c.va_list) callconv(.c) void { + syslog(fmt, @ptrCast(va_list)); +} + +pub fn log_timestamp() callconv(.c) u32 { + return @truncate(get_time_since_boot().to_us() * 1_000); +} + +pub const malloc_internal = malloc; + +pub fn realloc_internal() callconv(.c) void { + @panic("realloc_internal: not implemented"); +} + +pub const calloc_internal = calloc; + +pub fn zalloc_internal(len: usize) callconv(.c) ?*anyopaque { + if (malloc(len)) |ptr| { + @memset(@as([*]u8, @ptrCast(ptr))[0..len], 0); + return ptr; + } + + return null; +} + +pub const wifi_malloc = malloc; + +pub fn wifi_realloc() callconv(.c) void { + @panic("wifi_realloc: not implemented"); +} + +pub const wifi_calloc = calloc; +pub const wifi_zalloc = zalloc_internal; + +var wifi_queue_handle: ?*QueueWrapper = null; +var wifi_queue: QueueWrapper = undefined; + +pub fn wifi_create_queue(capacity: c_int, item_len: c_int) callconv(.c) ?*anyopaque { + log.debug("wifi_create_queue {} {}", .{ capacity, item_len }); + + std.debug.assert(wifi_queue_handle == null); + + const buf: []u8 = gpa.alloc(u8, @intCast(capacity * item_len)) catch { + log.warn("failed to allocate queue buffer", .{}); + return null; + }; + + wifi_queue = .{ + .inner = .init(buf), + .item_len = @intCast(item_len), + }; + wifi_queue_handle = &wifi_queue; + + log.debug(">>>> wifi queue create: {*}", .{&wifi_queue_handle}); + + return @ptrCast(&wifi_queue_handle); +} + +pub fn wifi_delete_queue(ptr: ?*anyopaque) callconv(.c) void { + log.debug("wifi_delete_queue {?}", .{ptr}); + + std.debug.assert(ptr == @as(?*anyopaque, @ptrCast(&wifi_queue_handle))); + + const queue: *?*QueueWrapper = @ptrCast(@alignCast(ptr)); + gpa.free(queue.*.?.inner.buffer); + + wifi_queue_handle = null; + wifi_queue = undefined; +} + +pub fn coex_init() callconv(.c) c_int { + log.debug("coex_init", .{}); + + return if (coex_enabled) c.coex_init() else 0; +} + +pub fn coex_deinit() callconv(.c) void { + log.debug("coex_deinit", .{}); + + if (coex_enabled) c.coex_deinit(); +} + +pub fn coex_enable() callconv(.c) c_int { + log.debug("coex_enable", .{}); + + return if (coex_enabled) c.coex_enable() else 0; +} + +pub fn coex_disable() callconv(.c) void { + log.debug("coex_disable", .{}); + + if (coex_enabled) c.coex_disable(); +} + +pub fn coex_status_get() callconv(.c) u32 { + log.debug("coex_status_get", .{}); + + return if (coex_enabled) c.coex_status_get() else 0; +} + +pub fn coex_condition_set() callconv(.c) void { + @panic("coex_condition_set"); +} + +pub fn coex_wifi_request( + event: u32, + latency: u32, + duration: u32, +) callconv(.c) c_int { + log.debug("coex_wifi_request", .{}); + + return if (coex_enabled) c.coex_wifi_request(event, latency, duration) else 0; +} + +pub fn coex_wifi_release(event: u32) callconv(.c) c_int { + log.debug("coex_wifi_release", .{}); + + return if (coex_enabled) c.coex_wifi_release(event) else 0; +} + +pub fn coex_wifi_channel_set( + primary: u8, + secondary: u8, +) callconv(.c) c_int { + log.debug("coex_wifi_channel_set", .{}); + + return if (coex_enabled) c.coex_wifi_channel_set(primary, secondary) else 0; +} + +pub fn coex_event_duration_get(event: u32, duration: [*c]u32) callconv(.c) c_int { + log.debug("coex_event_duration_get", .{}); + + return if (coex_enabled) c.coex_event_duration_get(event, duration) else 0; +} + +pub fn coex_pti_get(event: u32, pti: [*c]u8) callconv(.c) c_int { + log.debug("coex_pti_get", .{}); + + return if (coex_enabled) c.coex_pti_get(event, pti) else 0; +} + +pub fn coex_schm_status_bit_clear(@"type": u32, status: u32) callconv(.c) void { + log.debug("coex_schm_status_bit_clear", .{}); + + if (coex_enabled) c.coex_schm_status_bit_clear(@"type", status); +} + +pub fn coex_schm_status_bit_set(@"type": u32, status: u32) callconv(.c) void { + log.debug("coex_schm_status_bit_set", .{}); + + if (coex_enabled) c.coex_schm_status_bit_set(@"type", status); +} + +pub fn coex_schm_interval_set(interval: u32) callconv(.c) c_int { + log.debug("coex_schm_interval_set", .{}); + + return if (coex_enabled) c.coex_schm_interval_set(interval) else 0; +} + +pub fn coex_schm_interval_get() callconv(.c) u32 { + log.debug("coex_schm_interval_get", .{}); + + return if (coex_enabled) c.coex_schm_interval_get() else 0; +} + +pub fn coex_schm_curr_period_get() callconv(.c) u8 { + log.debug("coex_schm_curr_period_get", .{}); + + return if (coex_enabled) c.coex_schm_curr_period_get() else 0; +} + +pub fn coex_schm_curr_phase_get() callconv(.c) ?*anyopaque { + log.debug("coex_schm_curr_phase_get", .{}); + + return if (coex_enabled) c.coex_schm_curr_phase_get() else null; +} + +pub fn coex_schm_process_restart() callconv(.c) c_int { + log.debug("coex_schm_process_restart", .{}); + + return if (coex_enabled) c.coex_schm_process_restart() else 0; +} + +pub fn coex_schm_register_cb( + @"type": c_int, + callback: ?*const fn (c_int) callconv(.c) c_int, +) callconv(.c) c_int { + log.debug("coex_schm_register_cb", .{}); + + return if (coex_enabled) c.coex_schm_register_callback(@"type", callback) else 0; +} + +pub fn coex_register_start_cb(cb: ?*const fn () callconv(.c) c_int) callconv(.c) c_int { + log.debug("coex_register_start_cb", .{}); + + return if (coex_enabled) c.coex_register_start_cb(cb) else 0; +} + +extern fn ext_coex_schm_flexible_period_set(period: u8) i32; +pub fn coex_schm_flexible_period_set(period: u8) callconv(.c) c_int { + log.debug("coex_schm_flexible_period_set {}", .{period}); + + return if (coex_enabled) ext_coex_schm_flexible_period_set(period) else 0; +} + +extern fn ext_coex_schm_flexible_period_get() u8; +pub fn coex_schm_flexible_period_get() callconv(.c) u8 { + log.debug("coex_schm_flexible_period_get", .{}); + + return if (coex_enabled) ext_coex_schm_flexible_period_get() else 0; +} diff --git a/port/espressif/esp/src/hal/radio/timer.zig b/port/espressif/esp/src/hal/radio/timer.zig new file mode 100644 index 000000000..479191360 --- /dev/null +++ b/port/espressif/esp/src/hal/radio/timer.zig @@ -0,0 +1,178 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const c = @import("esp-wifi-driver"); +const microzig = @import("microzig"); +const time = microzig.drivers.time; + +const rtos = @import("../rtos.zig"); +const get_time_since_boot = @import("../time.zig").get_time_since_boot; + +const log = std.log.scoped(.esp_radio_timer); + +pub const CallbackFn = *const fn (?*anyopaque) callconv(.c) void; + +pub const Timer = struct { + ets_timer: *c.ets_timer, + callback: CallbackFn, + arg: ?*anyopaque, + deadline: time.Deadline, + periodic: ?time.Duration, + node: std.SinglyLinkedList.Node = .{}, +}; + +var reload_semaphore: rtos.Semaphore = .init(0, 1); +var mutex: rtos.Mutex = .{}; +var timer_list: std.SinglyLinkedList = .{}; + +pub fn init(gpa: Allocator) Allocator.Error!void { + _ = try rtos.spawn(gpa, task_fn, .{}, .{ + .name = "radio_timer", + .priority = .lowest, // TODO: what should the priority be? + .stack_size = 4096, + }); +} + +pub fn deinit() void { + // TODO: exit mechanism +} + +pub fn setfn( + gpa: std.mem.Allocator, + ets_timer: *c.ets_timer, + callback: CallbackFn, + arg: ?*anyopaque, +) !void { + { + mutex.lock(); + defer mutex.unlock(); + + if (find(ets_timer)) |tim| { + tim.callback = callback; + tim.arg = arg; + tim.deadline = .no_deadline; + } else { + const timer = try gpa.create(Timer); + timer.* = .{ + .ets_timer = ets_timer, + .callback = callback, + .arg = arg, + .deadline = .no_deadline, + .periodic = null, + }; + timer_list.prepend(&timer.node); + } + } + + reload_semaphore.give(); +} + +pub fn arm( + ets_timer: *c.ets_timer, + duration: time.Duration, + repeat: bool, +) void { + { + mutex.lock(); + defer mutex.unlock(); + + if (find(ets_timer)) |tim| { + tim.deadline = .init_relative(get_time_since_boot(), duration); + tim.periodic = if (repeat) duration else null; + } else { + log.warn("arm: timer not found based on ets_timer", .{}); + } + } + + reload_semaphore.give(); +} + +pub fn disarm(ets_timer: *c.ets_timer) void { + mutex.lock(); + defer mutex.unlock(); + + if (find(ets_timer)) |tim| { + tim.deadline = .no_deadline; + } else { + log.warn("disarm: timer not found based on ets_timer", .{}); + } +} + +pub fn done(gpa: std.mem.Allocator, ets_timer: *c.ets_timer) void { + mutex.lock(); + defer mutex.unlock(); + + if (find(ets_timer)) |tim| { + timer_list.remove(&tim.node); + gpa.destroy(tim); + } else { + log.warn("timer not found based on ets_timer", .{}); + } +} + +fn task_fn() void { + while (true) { + const now = get_time_since_boot(); + while (true) { + const callback, const arg = blk: { + mutex.lock(); + defer mutex.unlock(); + + const tim = find_expired(now) orelse break; + if (tim.periodic) |period| { + tim.deadline = .init_relative(now, period); + } else { + tim.deadline = .no_deadline; + } + break :blk .{ tim.callback, tim.arg }; + }; + + callback(arg); + } + + const sleep_duration = blk: { + mutex.lock(); + defer mutex.unlock(); + break :blk if (find_next_wake_absolute()) |next_wake_absolute| + next_wake_absolute.diff(now) + else + null; + }; + + reload_semaphore.take_with_timeout(sleep_duration) catch {}; + } +} + +fn find(ets_timer: *c.ets_timer) ?*Timer { + var it = timer_list.first; + while (it) |node| : (it = node.next) { + const timer: *Timer = @alignCast(@fieldParentPtr("node", node)); + if (timer.ets_timer == ets_timer) { + return timer; + } + } + return null; +} + +fn find_expired(now: time.Absolute) ?*Timer { + var it = timer_list.first; + while (it) |node| : (it = node.next) { + const timer: *Timer = @alignCast(@fieldParentPtr("node", node)); + if (timer.deadline.is_reached_by(now)) { + return timer; + } + } + return null; +} + +fn find_next_wake_absolute() ?time.Absolute { + var it = timer_list.first; + var min_deadline: time.Deadline = .no_deadline; + while (it) |node| : (it = node.next) { + const timer: *Timer = @alignCast(@fieldParentPtr("node", node)); + if (@intFromEnum(timer.deadline.timeout) < @intFromEnum(min_deadline.timeout)) { + min_deadline = timer.deadline; + } + } + return if (min_deadline.can_be_reached()) min_deadline.timeout else null; +} diff --git a/port/espressif/esp/src/hal/radio/wifi.zig b/port/espressif/esp/src/hal/radio/wifi.zig new file mode 100644 index 000000000..6aa4d79e6 --- /dev/null +++ b/port/espressif/esp/src/hal/radio/wifi.zig @@ -0,0 +1,1383 @@ +const std = @import("std"); +const assert = std.debug.assert; + +pub const c = @import("esp-wifi-driver"); +const microzig = @import("microzig"); +const wifi_options = microzig.options.hal.radio.wifi; +const time = microzig.drivers.time; + +const radio = @import("../radio.zig"); +const rtos = @import("../rtos.zig"); +const osi = @import("osi.zig"); + +const log = std.log.scoped(.esp_radio_wifi); + +pub const Options = struct { + on_event: ?*const fn (e: Event) void = null, + on_packet_received: ?*const fn (comptime interface: Interface, data: []const u8) void = null, + on_packet_transmitted: ?*const fn (interface: Interface, data: []const u8, status: bool) void = null, +}; + +const InitError = std.mem.Allocator.Error || InternalError; + +pub const InitConfig = struct { + /// Power save mode. + power_save_mode: PowerSaveMode = .none, + + /// Country info. If .auto the country info of the AP to which the station + /// is connected is used. + country_info: CountryInfo = .auto, + /// Set maximum transmitting power after WiFi start. + /// TODO: explain what values are valid + /// TODO: should it be part of country info? + max_tx_power: i8 = 20, + + /// Max number of WiFi static RX buffers. + /// + /// Each buffer takes approximately 1.6KB of RAM. The static rx buffers are + /// allocated when esp_wifi_init is called, they are not freed until + /// esp_wifi_deinit is called. + /// + /// WiFi hardware use these buffers to receive all 802.11 frames. A higher + /// number may allow higher throughput but increases memory use. If + /// [`Self::ampdu_rx_enable`] is enabled, this value is recommended to set + /// equal or bigger than [`Self::rx_ba_win`] in order to achieve better + /// throughput and compatibility with both stations and APs. + static_rx_buf_num: u8 = 10, + + /// Max number of WiFi dynamic RX buffers + /// + /// Set the number of WiFi dynamic RX buffers, 0 means unlimited RX buffers + /// will be allocated (provided sufficient free RAM). The size of each + /// dynamic RX buffer depends on the size of the received data frame. + /// + /// For each received data frame, the WiFi driver makes a copy to an RX + /// buffer and then delivers it to the high layer TCP/IP stack. The dynamic + /// RX buffer is freed after the higher layer has successfully received the + /// data frame. + /// + /// For some applications, WiFi data frames may be received faster than the + /// application can process them. In these cases we may run out of memory + /// if RX buffer number is unlimited (0). + /// + /// If a dynamic RX buffer limit is set, it should be at least the number + /// of static RX buffers. + dynamic_rx_buf_num: u16 = 32, + + /// Set the number of WiFi static TX buffers. + /// + /// Each buffer takes approximately 1.6KB of RAM. The static RX buffers are + /// allocated when esp_wifi_init() is called, they are not released until + /// esp_wifi_deinit() is called. + /// + /// For each transmitted data frame from the higher layer TCP/IP stack, the + /// WiFi driver makes a copy of it in a TX buffer. + /// + /// For some applications especially UDP applications, the upper layer can + /// deliver frames faster than WiFi layer can transmit. In these cases, we + /// may run out of TX buffers. + static_tx_buf_num: u8 = 0, + + /// Set the number of WiFi dynamic TX buffers. + /// + /// The size of each dynamic TX buffer is not fixed, it depends on the size + /// of each transmitted data frame. + /// + /// For each transmitted frame from the higher layer TCP/IP stack, the WiFi + /// driver makes a copy of it in a TX buffer. + /// + /// For some applications, especially UDP applications, the upper layer can + /// deliver frames faster than WiFi layer can transmit. In these cases, we + /// may run out of TX buffers. + dynamic_tx_buf_num: u16 = 32, + + /// Select this option to enable AMPDU RX feature. + ampdu_rx_enable: bool = true, + + /// Select this option to enable AMPDU TX feature. + ampdu_tx_enable: bool = true, + + /// Select this option to enable AMSDU TX feature. + amsdu_tx_enable: bool = false, + + /// Set the size of WiFi Block Ack RX window. + /// + /// Generally a bigger value means higher throughput and better + /// compatibility but more memory. Most of time we should NOT change the + /// default value unless special reason, e.g. test the maximum UDP RX + /// throughput with iperf etc. For iperf test in shieldbox, the recommended + /// value is 9~12. + /// + /// If PSRAM is used and WiFi memory is preferred to allocate in PSRAM + /// first, the default and minimum value should be 16 to achieve better + /// throughput and compatibility with both stations and APs. + rx_ba_win: u8 = 6, + + pub const CountryInfo = union(enum) { + auto, + manual: struct { + /// Country code. + country_code: *const [2]u8 = "01", + /// Start channel of the allowed 2.4GHz Wi-Fi channels. + start_channel: u8 = 1, + /// Total channel number of the allowed 2.4GHz Wi-Fi channels. + total_channel_number: u8 = 11, + }, + + fn get_wifi_country_t(info: CountryInfo, max_tx_power: i8) c.wifi_country_t { + return switch (info) { + .auto => .{ + .cc = .{ '0', '1', 0 }, + .schan = 1, + .nchan = 11, + .max_tx_power = max_tx_power, + .policy = c.WIFI_COUNTRY_POLICY_AUTO, + }, + .manual => |manual_info| .{ + .cc = manual_info.country_code.* ++ .{0}, + .schan = manual_info.start_channel, + .nchan = manual_info.total_channel_number, + .max_tx_power = max_tx_power, + .policy = c.WIFI_COUNTRY_POLICY_MANUAL, + }, + }; + } + }; +}; + +pub fn init(gpa: std.mem.Allocator, config: InitConfig) InitError!void { + comptime export_symbols(); + + try radio.init(gpa); + + { + init_config.wpa_crypto_funcs = c.g_wifi_default_wpa_crypto_funcs; + init_config.feature_caps = g_wifi_feature_caps; + init_config.static_rx_buf_num = config.static_rx_buf_num; + init_config.dynamic_rx_buf_num = config.dynamic_rx_buf_num; + init_config.static_tx_buf_num = config.static_tx_buf_num; + init_config.dynamic_tx_buf_num = config.dynamic_tx_buf_num; + init_config.ampdu_rx_enable = @intFromBool(config.ampdu_rx_enable); + init_config.ampdu_tx_enable = @intFromBool(config.ampdu_tx_enable); + init_config.amsdu_tx_enable = @intFromBool(config.amsdu_tx_enable); + init_config.rx_ba_win = config.rx_ba_win; + } + + // TODO: if coex enabled + if (false) try c_err(c.coex_init()); + + try c_err(c.esp_wifi_init_internal(&init_config)); + + try c_err(c.esp_wifi_set_mode(c.WIFI_MODE_NULL)); + + try c_err(c.esp_supplicant_init()); + + try c_err(c.esp_wifi_set_tx_done_cb(tx_done_cb)); + try c_err(c.esp_wifi_internal_reg_rxcb(c.ESP_IF_WIFI_AP, recv_cb_ap)); + try c_err(c.esp_wifi_internal_reg_rxcb(c.ESP_IF_WIFI_STA, recv_cb_sta)); + + { + const country_info = config.country_info.get_wifi_country_t(config.max_tx_power); + try c_err(c.esp_wifi_set_country(&country_info)); + } + + try set_power_save_mode(config.power_save_mode); +} + +pub fn deinit() void { + _ = c.esp_wifi_stop(); + _ = c.esp_wifi_deinit_internal(); + _ = c.esp_supplicant_deinit(); + + radio.deinit(); +} + +pub const ConfigError = InternalError || error{ + InvalidConfig, +}; + +pub const Config = union(enum) { + ap: AccessPoint, + sta: Station, + ap_sta: struct { + ap: AccessPoint, + sta: Station, + }, + // TODO: eap_sta + + pub const AccessPoint = struct { + /// The SSID of the access point. + ssid: []const u8, + + /// The password of the access point. + password: []const u8 = "", + + /// Whether the SSID is hidden or visible. + ssid_hidden: bool = false, + + /// The channel the access point will operate on. Ignored in `ap_sta` + /// mode. + channel: u8, + + /// The secondary channel configuration. + secondary_channel: ?u8 = null, + + /// Authentication config to be used by the access point. + auth_method: AuthMethod = .none, + + /// The maximum number of connections allowed on the access point. + max_connections: u8, + }; + + pub const Station = struct { + /// The SSID of the Wi-Fi network. + ssid: []const u8, + + /// The password of the Wi-Fi network. + password: []const u8 = "", + + /// The BSSID (MAC address) of the client. + bssid: ?[6]u8 = null, + + /// Authentication config for the Wi-Fi connection. + auth_method: AuthMethod = .none, + + /// The Wi-Fi channel to connect to. + channel: u8 = 0, + + scan_method: ScanMethod = .fast, + + listen_interval: u16 = 3, + + failure_retry_cnt: u8 = 1, + + pub const ScanMethod = enum(u32) { + fast = c.WIFI_FAST_SCAN, + all_channel = c.WIFI_ALL_CHANNEL_SCAN, + }; + }; +}; + +pub fn apply(config: Config) ConfigError!void { + switch (config) { + .ap => |ap_config| { + try set_mode(.ap); + try apply_access_point_config(ap_config); + }, + .sta => |sta_config| { + try set_mode(.sta); + try apply_station_config(sta_config); + }, + .ap_sta => |ap_sta_config| { + try set_mode(.ap_sta); + try apply_access_point_config(ap_sta_config.ap); + try apply_station_config(ap_sta_config.sta); + }, + } +} + +pub const WifiMode = enum(u32) { + sta = c.WIFI_MODE_STA, + ap = c.WIFI_MODE_AP, + ap_sta = c.WIFI_MODE_APSTA, + + pub fn is_sta(self: WifiMode) bool { + return self == .sta or self == .ap_sta; + } + + pub fn is_ap(self: WifiMode) bool { + return self == .ap or self == .ap_sta; + } +}; + +pub fn get_mode() InternalError!WifiMode { + var mode: c.wifi_mode_t = undefined; + try c_err(c.esp_wifi_get_mode(&mode)); + return @enumFromInt(mode); +} + +pub fn set_mode(mode: WifiMode) InternalError!void { + try c_err(c.esp_wifi_set_mode(@intFromEnum(mode))); +} + +fn apply_access_point_config(config: Config.AccessPoint) ConfigError!void { + if (config.ssid.len > 32) { + return error.InvalidConfig; + } + if (config.password.len > 64) { + return error.InvalidConfig; + } + + var ap_cfg: c_patched.wifi_ap_config_t = .{ + .ssid_len = @intCast(config.ssid.len), + .channel = config.channel, + .authmode = @intFromEnum(config.auth_method), + .ssid_hidden = @intFromBool(config.ssid_hidden), + .max_connection = config.max_connections, + .beacon_interval = 100, + .pairwise_cipher = c.WIFI_CIPHER_TYPE_CCMP, + .ftm_responder = false, + .pmf_cfg = .{ + .capable = true, + .required = false, + }, + .sae_pwe_h2e = 0, + .csa_count = 3, + .dtim_period = 2, + }; + + @memcpy(ap_cfg.ssid[0..config.ssid.len], config.ssid); + @memcpy(ap_cfg.password[0..config.password.len], config.password); + + var tmp: c_patched.wifi_config_t = .{ .ap = ap_cfg }; + try c_err(c_patched.esp_wifi_set_config(c.WIFI_IF_AP, &tmp)); +} + +fn apply_station_config(config: Config.Station) ConfigError!void { + if (config.ssid.len > 32) { + return error.InvalidConfig; + } + if (config.password.len > 64) { + return error.InvalidConfig; + } + + var sta_cfg: c_patched.wifi_sta_config_t = .{ + .scan_method = @intFromEnum(config.scan_method), + .bssid_set = config.bssid != null, + .bssid = config.bssid orelse @splat(0), + .channel = config.channel, + .listen_interval = config.listen_interval, + .sort_method = c.WIFI_CONNECT_AP_BY_SIGNAL, + .threshold = .{ + .rssi = -99, + .authmode = @intFromEnum(config.auth_method), + .rssi_5g_adjustment = 0, + }, + .pmf_cfg = .{ + .capable = true, + .required = false, + }, + .sae_pwe_h2e = 3, + .failure_retry_cnt = config.failure_retry_cnt, + }; + + @memcpy(sta_cfg.ssid[0..config.ssid.len], config.ssid); + @memcpy(sta_cfg.password[0..config.password.len], config.password); + + var tmp: c_patched.wifi_config_t = .{ .sta = sta_cfg }; + try c_err(c_patched.esp_wifi_set_config(c.WIFI_IF_STA, &tmp)); +} + +pub const PowerSaveMode = enum(u32) { + none = c.WIFI_PS_NONE, + min = c.WIFI_PS_MIN_MODEM, + max = c.WIFI_PS_MAX_MODEM, +}; + +pub fn set_power_save_mode(mode: PowerSaveMode) InternalError!void { + try c_err(c.esp_wifi_set_ps(@intFromEnum(mode))); +} + +pub const Protocol = enum(u8) { + /// 802.11b protocol. + P802D11B = c.WIFI_PROTOCOL_11B, + + /// 802.11b/g protocol. + P802D11BG = c.WIFI_PROTOCOL_11B | c.WIFI_PROTOCOL_11G, + + /// 802.11b/g/n protocol (default). + P802D11BGN = c.WIFI_PROTOCOL_11B | c.WIFI_PROTOCOL_11G | c.WIFI_PROTOCOL_11N, + + /// 802.11b/g/n long-range (LR) protocol. + P802D11BGNLR = c.WIFI_PROTOCOL_11B | c.WIFI_PROTOCOL_11G | c.WIFI_PROTOCOL_11N | c.WIFI_PROTOCOL_LR, + + /// 802.11 long-range (LR) protocol. + P802D11LR = c.WIFI_PROTOCOL_LR, + + /// 802.11b/g/n/ax protocol. + P802D11BGNAX = c.WIFI_PROTOCOL_11B | c.WIFI_PROTOCOL_11G | c.WIFI_PROTOCOL_11N | c.WIFI_PROTOCOL_11AX, +}; + +pub fn set_protocol(protocols: []const Config.AccessPoint.Protocol) InternalError!void { + var combined: u8 = 0; + for (protocols) |protocol| { + combined |= @intFromEnum(protocol); + } + + const mode = try get_mode(); + if (mode.is_sta()) { + try c_err(c.esp_wifi_set_protocol(c.WIFI_IF_STA, combined)); + } + if (mode.is_ap()) { + try c_err(c.esp_wifi_set_protocol(c.WIFI_IF_AP, combined)); + } +} + +/// Set the inactive time in seconds of the STA or AP. +/// +/// 1. For Station, if the station does not receive a beacon frame from the +/// connected SoftAP during the inactive time, disconnect from SoftAP. Default +/// 6s. Must be at least 3s. +/// +/// 2. For SoftAP, if the softAP doesn't receive any data from the connected +/// STA during inactive time, the softAP will force deauth the STA. Default is +/// 300s. Must be at least 10s. +pub fn set_inactive_time(interface: Interface, inactive_time: u32) InternalError!void { + try c_err(c.esp_wifi_set_inactive_time( + @intFromEnum(interface), + inactive_time, + )); +} + +pub fn start() InternalError!void { + try c_err(c.esp_wifi_start()); +} + +pub fn stop() InternalError!void { + try c_err(c.esp_wifi_stop()); +} + +/// Non-blocking. +pub fn connect() InternalError!void { + try c_err(c.esp_wifi_connect_internal()); +} + +pub fn disconnect() InternalError!void { + try c_err(c.esp_wifi_disconnect_internal()); +} + +pub fn start_blocking() InternalError!void { + const mode = try get_mode(); + var events: EventSet = .initEmpty(); + if (mode.is_sta()) events.setPresent(.StaStart, true); + if (mode.is_ap()) events.setPresent(.ApStart, true); + clear_events(events); + try start(); + wait_for_all_events(events); +} + +pub const ConnectError = error{FailedToConnect} || InternalError; + +pub fn connect_blocking() ConnectError!void { + const events: EventSet = .initMany(&.{ .StaConnected, .StaDisconnected }); + clear_events(events); + try connect(); + if (wait_for_any_event(events).contains(.StaDisconnected)) + return error.FailedToConnect; +} + +pub const EventType = enum(i32) { + /// Wi-Fi is ready for operation. + WifiReady = 0, + /// Scan operation has completed. + ScanDone, + /// Station mode started. + StaStart, + /// Station mode stopped. + StaStop, + /// Station connected to a network. + StaConnected, + /// Station disconnected from a network. + StaDisconnected, + /// Station authentication mode changed. + StaAuthmodeChange, + + /// Station WPS succeeds in enrollee mode. + StaWpsErSuccess, + /// Station WPS fails in enrollee mode. + StaWpsErFailed, + /// Station WPS timeout in enrollee mode. + StaWpsErTimeout, + /// Station WPS pin code in enrollee mode. + StaWpsErPin, + /// Station WPS overlap in enrollee mode. + StaWpsErPbcOverlap, + + /// Soft-AP start. + ApStart, + /// Soft-AP stop. + ApStop, + /// A station connected to Soft-AP. + ApStaconnected, + /// A station disconnected from Soft-AP. + ApStadisconnected, + /// Received probe request packet in Soft-AP interface. + ApProbereqrecved, + + /// Received report of FTM procedure. + FtmReport, + + /// AP's RSSI crossed configured threshold. + StaBssRssiLow, + /// Status indication of Action Tx operation. + ActionTxStatus, + /// Remain-on-Channel operation complete. + RocDone, + + /// Station beacon timeout. + StaBeaconTimeout, + + /// Connectionless module wake interval has started. + ConnectionlessModuleWakeIntervalStart, + + /// Soft-AP WPS succeeded in registrar mode. + ApWpsRgSuccess, + /// Soft-AP WPS failed in registrar mode. + ApWpsRgFailed, + /// Soft-AP WPS timed out in registrar mode. + ApWpsRgTimeout, + /// Soft-AP WPS pin code in registrar mode. + ApWpsRgPin, + /// Soft-AP WPS overlap in registrar mode. + ApWpsRgPbcOverlap, + + /// iTWT setup. + ItwtSetup, + /// iTWT teardown. + ItwtTeardown, + /// iTWT probe. + ItwtProbe, + /// iTWT suspended. + ItwtSuspend, + /// TWT wakeup event. + TwtWakeup, + /// bTWT setup. + BtwtSetup, + /// bTWT teardown. + BtwtTeardown, + + /// NAN (Neighbor Awareness Networking) discovery has started. + NanStarted, + /// NAN discovery has stopped. + NanStopped, + /// NAN service discovery match found. + NanSvcMatch, + /// Replied to a NAN peer with service discovery match. + NanReplied, + /// Received a follow-up message in NAN. + NanReceive, + /// Received NDP (Neighbor Discovery Protocol) request from a NAN peer. + NdpIndication, + /// NDP confirm indication. + NdpConfirm, + /// NAN datapath terminated indication. + NdpTerminated, + /// Wi-Fi home channel change, doesn't occur when scanning. + HomeChannelChange, + + /// Received Neighbor Report response. + StaNeighborRep, +}; + +pub const Event = union(EventType) { + /// Wi-Fi is ready for operation. + WifiReady, + /// Scan operation has completed. + ScanDone: c.wifi_event_sta_scan_done_t, + /// Station mode started. + StaStart, + /// Station mode stopped. + StaStop, + /// Station connected to a network. + StaConnected: c.wifi_event_sta_connected_t, + /// Station disconnected from a network. + StaDisconnected: c.wifi_event_sta_disconnected_t, + /// Station authentication mode changed. + StaAuthmodeChange: c.wifi_event_sta_authmode_change_t, + + /// Station WPS succeeds in enrollee mode. + StaWpsErSuccess: c.wifi_event_sta_wps_er_success_t, + /// Station WPS fails in enrollee mode. + StaWpsErFailed, + /// Station WPS timeout in enrollee mode. + StaWpsErTimeout, + /// Station WPS pin code in enrollee mode. + StaWpsErPin: c.wifi_event_sta_wps_er_pin_t, + /// Station WPS overlap in enrollee mode. + StaWpsErPbcOverlap, + + /// Soft-AP start. + ApStart, + /// Soft-AP stop. + ApStop, + /// A station connected to Soft-AP. + ApStaconnected: c.wifi_event_ap_staconnected_t, + /// A station disconnected from Soft-AP. + ApStadisconnected: c.wifi_event_ap_stadisconnected_t, + /// Received probe request packet in Soft-AP interface. + ApProbereqrecved: c.wifi_event_ap_probe_req_rx_t, + + /// Received report of FTM procedure. + FtmReport: c.wifi_event_ftm_report_t, + + /// AP's RSSI crossed configured threshold. + StaBssRssiLow: c.wifi_event_bss_rssi_low_t, + /// Status indication of Action Tx operation. + ActionTxStatus: c.wifi_event_action_tx_status_t, + /// Remain-on-Channel operation complete. + RocDone: c.wifi_event_roc_done_t, + + /// Station beacon timeout. + StaBeaconTimeout, + + /// Connectionless module wake interval has started. + ConnectionlessModuleWakeIntervalStart, + + /// Soft-AP WPS succeeded in registrar mode. + ApWpsRgSuccess: c.wifi_event_ap_wps_rg_success_t, + /// Soft-AP WPS failed in registrar mode. + ApWpsRgFailed: c.wifi_event_ap_wps_rg_fail_reason_t, + /// Soft-AP WPS timed out in registrar mode. + ApWpsRgTimeout, + /// Soft-AP WPS pin code in registrar mode. + ApWpsRgPin: c.wifi_event_ap_wps_rg_pin_t, + /// Soft-AP WPS overlap in registrar mode. + ApWpsRgPbcOverlap, + + /// iTWT setup. + ItwtSetup, + /// iTWT teardown. + ItwtTeardown, + /// iTWT probe. + ItwtProbe, + /// iTWT suspended. + ItwtSuspend, + /// TWT wakeup event. + TwtWakeup, + /// bTWT setup. + BtwtSetup, + /// bTWT teardown. + BtwtTeardown, + + /// NAN (Neighbor Awareness Networking) discovery has started. + NanStarted, + /// NAN discovery has stopped. + NanStopped, + /// NAN service discovery match found. + // TODO: c.wifi_event_nan_svc_match_t + NanSvcMatch, + /// Replied to a NAN peer with service discovery match. + // TODO: c.wifi_event_nan_replied_t + NanReplied, + /// Received a follow-up message in NAN. + // TODO: c.wifi_event_nan_receive_t + NanReceive, + /// Received NDP (Neighbor Discovery Protocol) request from a NAN peer. + // TODO: c.wifi_event_ndp_indication_t + NdpIndication, + /// NDP confirm indication. + // TODO: c.wifi_event_ndp_confirm_t + NdpConfirm, + /// NAN datapath terminated indication. + // TODO: c.wifi_event_ndp_terminated_t + NdpTerminated, + /// Wi-Fi home channel change, doesn't occur when scanning. + HomeChannelChange: c.wifi_event_home_channel_change_t, + + /// Received Neighbor Report response. + StaNeighborRep: c.wifi_event_neighbor_report_t, +}; + +pub const EventSet = std.EnumSet(EventType); + +var event_mutex: rtos.Mutex = .{}; +var event_condition: rtos.Condition = .{}; +var active_events: EventSet = .{}; + +pub fn wait_for_any_event(events: EventSet) EventSet { + event_mutex.lock(); + defer event_mutex.unlock(); + + while (true) { + const intersection = active_events.intersectWith(events); + if (intersection.count() > 0) return intersection; + event_condition.wait(&event_mutex); + } +} + +pub fn wait_for_all_events(events: EventSet) void { + event_mutex.lock(); + defer event_mutex.unlock(); + + while (!active_events.supersetOf(events)) { + event_condition.wait(&event_mutex); + } +} + +pub fn wait_for_event(event: EventType) void { + event_mutex.lock(); + defer event_mutex.unlock(); + + while (!active_events.contains(event)) { + event_condition.wait(&event_mutex); + } +} + +pub fn clear_events(events: EventSet) void { + event_mutex.lock(); + defer event_mutex.unlock(); + + active_events = active_events.differenceWith(events); +} + +/// Internal function. Called by osi layer. +pub fn on_event_post(id: i32, data: ?*anyopaque, data_size: usize) void { + log.debug("esp_event_post {} {?} {}", .{ id, data, data_size }); + + const event_type: EventType = @enumFromInt(id); + update_sta_state(event_type); + + { + event_mutex.lock(); + defer event_mutex.unlock(); + active_events.setPresent(event_type, true); + } + event_condition.broadcast(); + + const event = switch (event_type) { + inline else => |tag| blk: { + const PayloadType = @FieldType(Event, @tagName(tag)); + if (PayloadType == void) break :blk @unionInit(Event, @tagName(tag), {}); + + // Purposely skipping the assert if the type is void. There are + // some todos where we use void for the payload instead of the + // actual payload type. + assert(data_size == @sizeOf(PayloadType)); + + break :blk @unionInit(Event, @tagName(tag), @as(*PayloadType, @ptrCast(@alignCast(data))).*); + }, + }; + + if (wifi_options.on_event) |on_event| { + on_event(event); + } +} + +pub const StaState = enum(u32) { + none, + started, + connected, + disconnected, + stopped, + + pub fn is_started(state: StaState) bool { + return switch (state) { + .started, .connected, .disconnected => true, + else => false, + }; + } +}; + +var sta_state: std.atomic.Value(StaState) = .init(.none); + +fn update_sta_state(event: EventType) void { + const new_sta_state: StaState = switch (event) { + .StaStart => .started, + .StaConnected => .connected, + .StaDisconnected => .disconnected, + .StaStop => .stopped, + else => return, + }; + sta_state.store(new_sta_state, .monotonic); +} + +pub fn get_sta_state() StaState { + return sta_state.load(.monotonic); +} + +pub const Interface = enum(u32) { + ap = c.WIFI_IF_AP, + sta = c.WIFI_IF_STA, + + pub fn is_active(self: Interface) InternalError!bool { + const mode = try get_mode(); + return switch (self) { + .ap => mode.is_ap(), + .sta => mode.is_sta(), + }; + } +}; + +pub fn send_packet(iface: Interface, data: []const u8) (error{TooManyPacketsInFlight} || InternalError)!void { + try c_err(c.esp_wifi_internal_tx(@intFromEnum(iface), @ptrCast(@constCast(data.ptr)), @intCast(data.len))); +} + +fn tx_done_cb( + iface_idx: u8, + data_ptr: [*c]u8, + data_len: [*c]u16, + status: bool, +) callconv(.c) void { + log.debug("tx_done_cb", .{}); + + if (wifi_options.on_packet_transmitted) |on_packet_transmitted| + on_packet_transmitted(@intFromEnum(iface_idx), data_ptr[0..data_len], status); +} + +fn recv_cb_ap(buf: ?*anyopaque, len: u16, eb: ?*anyopaque) callconv(.c) c.esp_err_t { + log.debug("recv_cb_ap {?} {} {?}", .{ buf, len, eb }); + + if (wifi_options.on_packet_received) |on_packet_received| { + on_packet_received(.ap, @as([*]const u8, @ptrCast(buf))[0..len]); + } + c.esp_wifi_internal_free_rx_buffer(eb); + + return c.ESP_OK; +} + +fn recv_cb_sta(buf: ?*anyopaque, len: u16, eb: ?*anyopaque) callconv(.c) c.esp_err_t { + log.debug("recv_cb_sta {?} {} {?}", .{ buf, len, eb }); + + if (wifi_options.on_packet_received) |on_packet_received| { + on_packet_received(.sta, @as([*]const u8, @ptrCast(buf))[0..len]); + } + c.esp_wifi_internal_free_rx_buffer(eb); + + return c.ESP_OK; +} + +pub const ScanConfig = struct { + ssid: ?[:0]const u8 = null, + bssid: ?[:0]const u8 = null, + channel: ?u8 = null, + show_hidden: bool = true, + type: Type = .{ .active = .{ + .min = .from_ms(100), + .max = .from_ms(200), + } }, + + pub const Type = union(enum) { + /// Active scan with min and max scan time per channel. This is the default + /// and recommended if you are unsure. + /// + /// # Procedure + /// 1. Send probe request on each channel. + /// 2. Wait for probe response. Wait at least `min` time, but if no response is received, wait + /// up to `max` time. + /// 3. Switch channel. + /// 4. Repeat from 1. + active: struct { + /// Minimum scan time per channel. + min: time.Duration, + /// Maximum scan time per channel. + max: time.Duration, + }, + /// Passive scan. + /// + /// # Procedure + /// 1. Wait for beacon for given duration. + /// 2. Switch channel. + /// 3. Repeat from 1. + passive: time.Duration, + }; +}; + +/// Blocks until the scan is finished. The returned iterator must be deinited +/// or consumed before another call to scan. +pub fn scan(config: ScanConfig) ConfigError!ScanResultsIterator { + const scan_time: c.wifi_scan_time_t = switch (config.type) { + .active => |active_time| .{ + .active = .{ + .min = @intCast(active_time.min.to_us() / 1000), + .max = @intCast(active_time.max.to_us() / 1000), + }, + }, + .passive => |passive_time| blk: { + if (!passive_time.less_than(.from_ms(1_500))) + return error.InvalidConfig; + + break :blk .{ .passive = @intCast(passive_time.to_us() / 1000) }; + }, + }; + + var scan_config: c.wifi_scan_config_t = .{ + .ssid = if (config.ssid) |ssid| @constCast(ssid.ptr) else null, + .bssid = if (config.bssid) |bssid| @constCast(bssid.ptr) else null, + .channel = config.channel orelse 0, + .show_hidden = config.show_hidden, + .scan_type = switch (config.type) { + .active => c.WIFI_SCAN_TYPE_ACTIVE, + .passive => c.WIFI_SCAN_TYPE_PASSIVE, + }, + .scan_time = scan_time, + .home_chan_dwell_time = 0, + .channel_bitmap = .{ + .ghz_2_channels = 0, + .ghz_5_channels = 0, + }, + .coex_background_scan = false, + }; + try c_err(c.esp_wifi_scan_start(&scan_config, true)); + return try .init(); +} + +/// SAFETY: There can be only one instance of `ScanIterator` at any given time. +pub const ScanResultsIterator = struct { + remaining: u16, + ap_record: c_patched.wifi_ap_record_t = undefined, + + /// Blocks the current thread until the scan finishes and returns an + /// iterator for the scan results. + pub fn init() InternalError!ScanResultsIterator { + var ap_count: u16 = undefined; + try c_err(c.esp_wifi_scan_get_ap_num(&ap_count)); + return .{ .remaining = ap_count }; + } + + pub fn deinit(_: ScanResultsIterator) void { + // free any ap records that were not returned + c_err(c.esp_wifi_clear_ap_list()) catch |err| { + std.log.warn("failed to clear ap list: {t}", .{err}); + }; + } + + /// SAFETY: AP_Record strings last until the next call to next. + pub fn next(self: *ScanResultsIterator) InternalError!?AP_Record { + if (self.remaining == 0) { + return null; + } + self.remaining -= 1; + + try c_err(c_patched.esp_wifi_scan_get_ap_record(&self.ap_record)); + + const ssid = std.mem.sliceTo(&self.ap_record.ssid, 0); + return .{ + .ssid = ssid, + .primary_channel = self.ap_record.primary, + .secondary_channel = self.ap_record.second, + .rssi = self.ap_record.rssi, + .auth_mode = std.enums.fromInt(AuthMethod, self.ap_record.authmode), + }; + } + + /// Strings (eg: ssid) have the same lifetime as the iterator object. + pub const AP_Record = struct { + ssid: []const u8, + primary_channel: u8, + secondary_channel: u32, + rssi: i8, + // TODO: this doesn't contain possibilities + auth_mode: ?AuthMethod, + // TODO: add the rest + }; +}; + +pub const AuthMethod = enum(u32) { + none = c.WIFI_AUTH_OPEN, + + /// Wired Equivalent Privacy (WEP) authentication. + wep = c.WIFI_AUTH_WEP, + + /// Wi-Fi Protected Access (WPA) authentication. + wpa = c.WIFI_AUTH_WPA_PSK, + + /// Wi-Fi Protected Access 2 (WPA2) Personal authentication (default). + wpa2_personal = c.WIFI_AUTH_WPA2_PSK, + + /// WPA/WPA2 Personal authentication (supports both). + wpa_wpa2_personal = c.WIFI_AUTH_WPA_WPA2_PSK, + + /// WPA2 Enterprise authentication. + wpa2_enterprise = c.WIFI_AUTH_WPA2_ENTERPRISE, + + /// WPA3 Personal authentication. + wpa3_personal = c.WIFI_AUTH_WPA3_PSK, + + /// WPA2/WPA3 Personal authentication (supports both). + wpa2_wpa3_personal = c.WIFI_AUTH_WPA2_WPA3_PSK, + + /// WLAN Authentication and Privacy Infrastructure (WAPI). + wapi_personal = c.WIFI_AUTH_WAPI_PSK, +}; + +fn export_symbols() void { + @export(&g_wifi_feature_caps, .{ .name = "g_wifi_feature_caps" }); + @export(&g_wifi_osi_funcs, .{ .name = "g_wifi_osi_funcs" }); +} + +// TODO: configurable +var init_config: c.wifi_init_config_t = .{ + .osi_funcs = &g_wifi_osi_funcs, + // .wpa_crypto_funcs = c.g_wifi_default_wpa_crypto_funcs, + .static_rx_buf_num = 10, // overwritten by init + .dynamic_rx_buf_num = 32, // overwritten by init + .tx_buf_type = c.CONFIG_ESP_WIFI_TX_BUFFER_TYPE, + .static_tx_buf_num = 0, // overwritten by init + .dynamic_tx_buf_num = 32, // overwritten by init + .rx_mgmt_buf_type = c.CONFIG_ESP_WIFI_STATIC_RX_MGMT_BUFFER, + .rx_mgmt_buf_num = c.CONFIG_ESP_WIFI_RX_MGMT_BUF_NUM_DEF, + .cache_tx_buf_num = c.WIFI_CACHE_TX_BUFFER_NUM, + .csi_enable = 0, // TODO: WiFi channel state information enable flag. + .ampdu_rx_enable = 1, // overwritten by init + .ampdu_tx_enable = 1, // overwritten by init + .amsdu_tx_enable = 0, // overwritten by init + .nvs_enable = 0, + .nano_enable = 0, + .rx_ba_win = 6, // overwritten by init + .wifi_task_core_id = 0, + .beacon_max_len = c.WIFI_SOFTAP_BEACON_MAX_LEN, + .mgmt_sbuf_num = c.WIFI_MGMT_SBUF_NUM, + .feature_caps = c.WIFI_FEATURE_CAPS, + .sta_disconnected_pm = false, + .espnow_max_encrypt_num = c.CONFIG_ESP_WIFI_ESPNOW_MAX_ENCRYPT_NUM, + .tx_hetb_queue_num = 3, + .dump_hesigb_enable = false, + .magic = c.WIFI_INIT_CONFIG_MAGIC, +}; + +const wifi_enable_wpa3_sae: u64 = 1 << 0; +const wifi_enable_enterprise: u64 = 1 << 7; +// const wifi_ftm_initiator: u64 = 1 << 2; +// const wifi_ftm_responder: u64 = 1 << 3; +// const wifi_enable_gcmp: u64 = 1 << 4; +// const wifi_enable_gmac: u64 = 1 << 5; +// const wifi_enable_11r: u64 = 1 << 6; + +var g_wifi_feature_caps: u64 = wifi_enable_wpa3_sae | wifi_enable_enterprise; + +var g_wifi_osi_funcs: c.wifi_osi_funcs_t = .{ + ._version = c.ESP_WIFI_OS_ADAPTER_VERSION, + ._env_is_chip = osi.env_is_chip, + ._set_intr = osi.set_intr, + ._clear_intr = osi.clear_intr, + ._set_isr = osi.set_isr, + ._ints_on = osi.ints_on, + ._ints_off = osi.ints_off, + ._is_from_isr = osi.is_from_isr, + ._spin_lock_create = osi.spin_lock_create, + ._spin_lock_delete = osi.spin_lock_delete, + ._wifi_int_disable = osi.wifi_int_disable, + ._wifi_int_restore = osi.wifi_int_restore, + ._task_yield_from_isr = osi.task_yield_from_isr, + ._semphr_create = osi.semphr_create, + ._semphr_delete = osi.semphr_delete, + ._semphr_take = osi.semphr_take, + ._semphr_give = osi.semphr_give, + ._wifi_thread_semphr_get = osi.wifi_thread_semphr_get, + ._mutex_create = osi.mutex_create, + ._recursive_mutex_create = osi.recursive_mutex_create, + ._mutex_delete = osi.mutex_delete, + ._mutex_lock = osi.mutex_lock, + ._mutex_unlock = osi.mutex_unlock, + ._queue_create = osi.queue_create, + ._queue_delete = osi.queue_delete, + ._queue_send = osi.queue_send, + ._queue_send_from_isr = osi.queue_send_from_isr, + ._queue_send_to_back = @ptrCast(&osi.queue_send_to_back), + ._queue_send_to_front = @ptrCast(&osi.queue_send_to_front), + ._queue_recv = osi.queue_recv, + ._queue_msg_waiting = osi.queue_msg_waiting, + ._event_group_create = @ptrCast(&osi.event_group_create), + ._event_group_delete = @ptrCast(&osi.event_group_delete), + ._event_group_set_bits = @ptrCast(&osi.event_group_set_bits), + ._event_group_clear_bits = @ptrCast(&osi.event_group_clear_bits), + ._event_group_wait_bits = @ptrCast(&osi.event_group_wait_bits), + ._task_create_pinned_to_core = osi.task_create_pinned_to_core, + ._task_create = osi.task_create, + ._task_delete = osi.task_delete, + ._task_delay = osi.task_delay, + ._task_ms_to_tick = osi.task_ms_to_tick, + ._task_get_current_task = osi.task_get_current_task, + ._task_get_max_priority = osi.task_get_max_priority, + ._malloc = osi.malloc, + ._free = osi.free, + ._event_post = osi.esp_event_post, + ._get_free_heap_size = @ptrCast(&osi.get_free_heap_size), + ._rand = osi.rand, + ._dport_access_stall_other_cpu_start_wrap = osi.dport_access_stall_other_cpu_start_wrap, + ._dport_access_stall_other_cpu_end_wrap = osi.dport_access_stall_other_cpu_end_wrap, + ._wifi_apb80m_request = osi.wifi_apb80m_request, + ._wifi_apb80m_release = osi.wifi_apb80m_release, + ._phy_disable = osi.phy_disable, + ._phy_enable = osi.phy_enable, + ._phy_update_country_info = osi.phy_update_country_info, + ._read_mac = osi.read_mac, + ._timer_arm = osi.timer_arm, + ._timer_disarm = osi.timer_disarm, + ._timer_done = osi.timer_done, + ._timer_setfn = osi.timer_setfn, + ._timer_arm_us = osi.timer_arm_us, + ._wifi_reset_mac = osi.wifi_reset_mac, + ._wifi_clock_enable = osi.wifi_clock_enable, + ._wifi_clock_disable = osi.wifi_clock_disable, + ._wifi_rtc_enable_iso = @ptrCast(&osi.wifi_rtc_enable_iso), + ._wifi_rtc_disable_iso = @ptrCast(&osi.wifi_rtc_disable_iso), + ._esp_timer_get_time = osi.esp_timer_get_time, + ._nvs_set_i8 = @ptrCast(&osi.nvs_set_i8), + ._nvs_get_i8 = @ptrCast(&osi.nvs_get_i8), + ._nvs_set_u8 = @ptrCast(&osi.nvs_set_u8), + ._nvs_get_u8 = @ptrCast(&osi.nvs_get_u8), + ._nvs_set_u16 = @ptrCast(&osi.nvs_set_u16), + ._nvs_get_u16 = @ptrCast(&osi.nvs_get_u16), + ._nvs_open = @ptrCast(&osi.nvs_open), + ._nvs_close = @ptrCast(&osi.nvs_close), + ._nvs_commit = @ptrCast(&osi.nvs_commit), + ._nvs_set_blob = @ptrCast(&osi.nvs_set_blob), + ._nvs_get_blob = @ptrCast(&osi.nvs_get_blob), + ._nvs_erase_key = @ptrCast(&osi.nvs_erase_key), + ._get_random = osi.get_random, + ._get_time = @ptrCast(&osi.get_time), + ._random = &osi.random, + ._slowclk_cal_get = osi.slowclk_cal_get, + ._log_write = osi.log_write, + ._log_writev = osi.log_writev, + ._log_timestamp = osi.log_timestamp, + ._malloc_internal = osi.malloc_internal, + ._realloc_internal = @ptrCast(&osi.realloc_internal), + ._calloc_internal = osi.calloc_internal, + ._zalloc_internal = osi.zalloc_internal, + ._wifi_malloc = osi.wifi_malloc, + ._wifi_realloc = @ptrCast(&osi.wifi_realloc), + ._wifi_calloc = osi.wifi_calloc, + ._wifi_zalloc = osi.wifi_zalloc, + ._wifi_create_queue = osi.wifi_create_queue, + ._wifi_delete_queue = osi.wifi_delete_queue, + ._coex_init = osi.coex_init, + ._coex_deinit = osi.coex_deinit, + ._coex_enable = osi.coex_enable, + ._coex_disable = osi.coex_disable, + ._coex_status_get = osi.coex_status_get, + ._coex_condition_set = @ptrCast(&osi.coex_condition_set), + ._coex_wifi_request = osi.coex_wifi_request, + ._coex_wifi_release = osi.coex_wifi_release, + ._coex_wifi_channel_set = osi.coex_wifi_channel_set, + ._coex_event_duration_get = osi.coex_event_duration_get, + ._coex_pti_get = osi.coex_pti_get, + ._coex_schm_status_bit_clear = osi.coex_schm_status_bit_clear, + ._coex_schm_status_bit_set = osi.coex_schm_status_bit_set, + ._coex_schm_interval_set = osi.coex_schm_interval_set, + ._coex_schm_interval_get = osi.coex_schm_interval_get, + ._coex_schm_curr_period_get = osi.coex_schm_curr_period_get, + ._coex_schm_curr_phase_get = osi.coex_schm_curr_phase_get, + ._coex_schm_process_restart = osi.coex_schm_process_restart, + ._coex_schm_register_cb = osi.coex_schm_register_cb, + ._coex_register_start_cb = osi.coex_register_start_cb, + ._coex_schm_flexible_period_set = osi.coex_schm_flexible_period_set, + ._coex_schm_flexible_period_get = osi.coex_schm_flexible_period_get, + ._magic = @bitCast(c.ESP_WIFI_OS_ADAPTER_MAGIC), +}; + +pub const InternalError = error{ + OutOfMemory, + InvalidArg, + Timeout, + WouldBlock, + WifiNotInitialized, + WifiNotStarted, + WifiNotStopped, + WifiNotConnected, + WifiInvalid_MAC, + WifiInvalid_SSID, + WifiInvalidPassword, + WifiOther, +}; + +pub fn c_err(raw_err_code: i32) InternalError!void { + const InternalWifiErrorCode = enum(i32) { + /// Out of memory + esp_err_no_mem = 0x101, + + /// Invalid argument + esp_err_invalid_arg = 0x102, + + /// Wi_fi driver was not installed by esp_wifi_init + esp_err_wifi_not_init = 0x3001, + + /// Wi_fi driver was not started by esp_wifi_start + esp_err_wifi_not_started = 0x3002, + + /// Wi_fi driver was not stopped by esp_wifi_stop + esp_err_wifi_not_stopped = 0x3003, + + /// Wi_fi interface error + esp_err_wifi_if = 0x3004, + + /// Wi_fi mode error + esp_err_wifi_mode = 0x3005, + + /// Wi_fi internal state error + esp_err_wifi_state = 0x3006, + + /// Wi_fi internal control block of station or soft-AP error + esp_err_wifi_conn = 0x3007, + + /// Wi_fi internal NVS module error + esp_err_wifi_nvs = 0x3008, + + /// MAC address is invalid + esp_err_wifi_mac = 0x3009, + + /// SSID is invalid + esp_err_wifi_ssid = 0x300A, + + /// Password is invalid + esp_err_wifi_password = 0x300B, + + /// Timeout error + esp_err_wifi_timeout = 0x300C, + + /// Wi_fi is in sleep state(RF closed) and wakeup fail + esp_err_wifi_wake_fail = 0x300D, + + /// The caller would block + esp_err_wifi_would_block = 0x300E, + + /// Station still in disconnect status + esp_err_wifi_not_connect = 0x300F, + + /// Failed to post the event to Wi_fi task + esp_err_wifi_post = 0x3012, + + /// Invalid Wi_fi state when init/deinit is called + esp_err_wifi_init_state = 0x3013, + + /// Returned when Wi_fi is stopping + esp_err_wifi_stop_state = 0x3014, + + /// The Wi_fi connection is not associated + esp_err_wifi_not_assoc = 0x3015, + + /// The Wi_fi TX is disallowed + esp_err_wifi_tx_disallow = 0x3016, + }; + + if (raw_err_code != c.ESP_OK) { + const err_code = std.enums.fromInt(InternalWifiErrorCode, raw_err_code) orelse + @panic("esp wifi returned unexpected error code"); + log.debug("internal wifi error occurred: {t}", .{err_code}); + return switch (err_code) { + .esp_err_no_mem => error.OutOfMemory, + .esp_err_wifi_timeout => error.Timeout, + .esp_err_wifi_would_block => error.WouldBlock, + .esp_err_wifi_not_init => error.WifiNotInitialized, + .esp_err_wifi_not_started => error.WifiNotStarted, + .esp_err_wifi_not_connect => error.WifiNotConnected, + .esp_err_wifi_not_stopped => error.WifiNotStopped, + .esp_err_wifi_mac => error.WifiInvalid_MAC, + .esp_err_wifi_ssid => error.WifiInvalid_SSID, + .esp_err_wifi_password => error.WifiInvalidPassword, + else => error.WifiOther, + }; + } +} + +pub const c_patched = struct { + pub const wifi_ap_config_t = extern struct { + ssid: [32]u8 = std.mem.zeroes([32]u8), + password: [64]u8 = std.mem.zeroes([64]u8), + ssid_len: u8 = std.mem.zeroes(u8), + channel: u8 = std.mem.zeroes(u8), + authmode: c.wifi_auth_mode_t = std.mem.zeroes(c.wifi_auth_mode_t), + ssid_hidden: u8 = std.mem.zeroes(u8), + max_connection: u8 = std.mem.zeroes(u8), + beacon_interval: u16 = std.mem.zeroes(u16), + csa_count: u8 = std.mem.zeroes(u8), + dtim_period: u8 = std.mem.zeroes(u8), + pairwise_cipher: c.wifi_cipher_type_t = std.mem.zeroes(c.wifi_cipher_type_t), + ftm_responder: bool = std.mem.zeroes(bool), + pmf_cfg: c.wifi_pmf_config_t = std.mem.zeroes(c.wifi_pmf_config_t), + sae_pwe_h2e: c.wifi_sae_pwe_method_t = std.mem.zeroes(c.wifi_sae_pwe_method_t), + }; + + pub const wifi_sta_config_t = extern struct { + ssid: [32]u8 = std.mem.zeroes([32]u8), + password: [64]u8 = std.mem.zeroes([64]u8), + scan_method: c.wifi_scan_method_t = std.mem.zeroes(c.wifi_scan_method_t), + bssid_set: bool = std.mem.zeroes(bool), + bssid: [6]u8 = std.mem.zeroes([6]u8), + channel: u8 = std.mem.zeroes(u8), + listen_interval: u16 = std.mem.zeroes(u16), + sort_method: c.wifi_sort_method_t = std.mem.zeroes(c.wifi_sort_method_t), + threshold: c.wifi_scan_threshold_t = std.mem.zeroes(c.wifi_scan_threshold_t), + pmf_cfg: c.wifi_pmf_config_t = std.mem.zeroes(c.wifi_pmf_config_t), + packed1: Packed1 = std.mem.zeroes(Packed1), + sae_pwe_h2e: c.wifi_sae_pwe_method_t = std.mem.zeroes(c.wifi_sae_pwe_method_t), + sae_pk_mode: c.wifi_sae_pk_mode_t = std.mem.zeroes(c.wifi_sae_pk_mode_t), + failure_retry_cnt: u8 = std.mem.zeroes(u8), + packed2: Packed2 = std.mem.zeroes(Packed2), + sae_h2e_identifier: [32]u8 = std.mem.zeroes([32]u8), + + // NOTE: maybe a little more imagination + pub const Packed1 = packed struct { + rm_enabled: bool, + btm_enabled: bool, + mbo_enabled: bool, + ft_enabled: bool, + owe_enabled: bool, + transition_disable: bool, + reserved: u26, + }; + + pub const Packed2 = packed struct { + he_dcm_set: u1, + he_dcm_max_constellation_tx: u2, + he_dcm_max_constellation_rx: u2, + he_mcs9_enabled: u1, + he_su_beamformee_disabled: u1, + he_trig_su_bmforming_feedback_disabled: u1, + he_trig_mu_bmforming_partial_feedback_disabled: u1, + he_trig_cqi_feedback_disable: u1, + he_reserved: u22, + }; + }; + + pub const wifi_nan_config_t = extern struct { + op_channel: u8 = std.mem.zeroes(u8), + master_pref: u8 = std.mem.zeroes(u8), + scan_time: u8 = std.mem.zeroes(u8), + warm_up_sec: u16 = std.mem.zeroes(u16), + }; + + pub const wifi_config_t = extern union { + ap: wifi_ap_config_t, + sta: wifi_sta_config_t, + nan: wifi_nan_config_t, + }; + + pub extern fn esp_wifi_set_config(interface: c.wifi_interface_t, conf: ?*wifi_config_t) c.esp_err_t; + + pub const wifi_ap_record_t = extern struct { + bssid: [6]u8 = std.mem.zeroes([6]u8), + ssid: [33]u8 = std.mem.zeroes([33]u8), + primary: u8 = std.mem.zeroes(u8), + second: c.wifi_second_chan_t = std.mem.zeroes(c.wifi_second_chan_t), + rssi: i8 = std.mem.zeroes(i8), + authmode: c.wifi_auth_mode_t = std.mem.zeroes(c.wifi_auth_mode_t), + pairwise_cipher: c.wifi_cipher_type_t = std.mem.zeroes(c.wifi_cipher_type_t), + group_cipher: c.wifi_cipher_type_t = std.mem.zeroes(c.wifi_cipher_type_t), + ant: c.wifi_ant_t = std.mem.zeroes(c.wifi_ant_t), + packed1: Packed1 = std.mem.zeroes(Packed1), + country: c.wifi_country_t = std.mem.zeroes(c.wifi_country_t), + he_ap: wifi_he_ap_info_t = std.mem.zeroes(wifi_he_ap_info_t), + bandwidth: c.wifi_bandwidth_t = std.mem.zeroes(c.wifi_bandwidth_t), + vht_ch_freq1: u8 = std.mem.zeroes(u8), + vht_ch_freq2: u8 = std.mem.zeroes(u8), + + pub const Packed1 = packed struct(u32) { + phy_11b: u1, + phy_11g: u1, + phy_11n: u1, + phy_lr: u1, + phy_11a: u1, + phy_11ac: u1, + phy_11ax: u1, + wps: u1, + ftm_responder: u1, + ftm_initiator: u1, + reserved: u22, + }; + }; + + pub const wifi_he_ap_info_t = extern struct { + packed1: Packed1, + bssid_index: u8, + + pub const Packed1 = packed struct(u8) { + bss_color: u6, + partial_bss_color: u1, + bss_color_disabled: u1, + }; + }; + + pub extern fn esp_wifi_scan_get_ap_record(ap_record: ?*wifi_ap_record_t) c.esp_err_t; +}; diff --git a/port/espressif/esp/src/hal/rtos.zig b/port/espressif/esp/src/hal/rtos.zig new file mode 100644 index 000000000..2ce93e1fb --- /dev/null +++ b/port/espressif/esp/src/hal/rtos.zig @@ -0,0 +1,1120 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const log = std.log.scoped(.rtos); +const builtin = @import("builtin"); + +const microzig = @import("microzig"); +const CriticalSection = microzig.interrupt.CriticalSection; +const enter_critical_section = microzig.interrupt.enter_critical_section; +const TrapFrame = microzig.cpu.TrapFrame; +const SYSTEM = microzig.chip.peripherals.SYSTEM; +const time = microzig.drivers.time; +const rtos_options = microzig.options.hal.rtos; +pub const Priority = rtos_options.Priority; + +const get_time_since_boot = @import("time.zig").get_time_since_boot; +const system = @import("system.zig"); +const systimer = @import("systimer.zig"); + +// How it works? +// +// For task to task context switches, only required registers are +// saved through the use of inline assembly clobbers. If a higher priority task +// becomes ready during the handling of an interrupt, a task switch is forced +// by saving the entire state of the task on the stack. What is interesting is +// that the two context switches are compatible. Voluntary yield can resume a +// task that was interrupted by force and vice versa. Because of the forced +// yield, tasks are required to have a minimum stack size available at all +// times. + +// TODO: stack overflow detection +// TODO: task joining and deletion +// - the idea is that tasks must return before they can be freed +// TODO: direct task signaling +// TODO: implement std.Io +// TODO: use @stackUpperBound when implemented +// TODO: better handling if timeout is in the past or very short +// TODO: support SMP for other esp32 chips with multicore (far future) + +const STACK_ALIGN: std.mem.Alignment = .@"16"; +const EXTRA_STACK_SIZE = @max(@sizeOf(TrapFrame), 32 * @sizeOf(usize)); + +pub const Options = struct { + enable: bool = false, + Priority: type = enum(u8) { + idle = 0, + lowest = 1, + _, + + pub const highest: @This() = @enumFromInt(std.math.maxInt(@typeInfo(@This()).@"enum".tag_type)); + }, + general_purpose_interrupt: microzig.cpu.Interrupt = .interrupt30, + systimer_unit: systimer.Unit = .unit0, + systimer_alarm: systimer.Alarm = .alarm0, + cpu_interrupt: system.CPU_Interrupt = .cpu_interrupt_0, + yield_interrupt: microzig.cpu.Interrupt = .interrupt31, + + paint_stack_byte: ?u8 = null, +}; + +var main_task: Task = .{ + .name = "main", + .context = undefined, + .priority = .lowest, + .stack = &.{}, +}; +var idle_stack: [STACK_ALIGN.forward(EXTRA_STACK_SIZE)]u8 align(STACK_ALIGN.toByteUnits()) = undefined; +var idle_task: Task = .{ + .name = "idle", + .context = .{ + .pc = &idle, + .sp = idle_stack[idle_stack.len..].ptr, + .fp = null, + }, + .priority = .idle, + .stack = &idle_stack, +}; + +var rtos_state: RTOS_State = undefined; +pub const RTOS_State = struct { + ready_queue: ReadyPriorityQueue = .{}, + timer_queue: std.DoublyLinkedList = .{}, + suspended_list: std.DoublyLinkedList = .{}, + scheduled_for_deletion_list: std.DoublyLinkedList = .{}, + + /// The task in .running state. Safe to access outside of critical section + /// as it is always the same for the currently executing task. + current_task: *Task, +}; + +/// Automatically called inside hal startup sequence if it the rtos is enabled +/// in hal options. +pub fn init() void { + comptime { + if (!microzig.options.cpu.interrupt_stack.enable) + @compileError("rtos requires the interrupt stack cpu option to be enabled"); + microzig.cpu.interrupt.expect_handler(rtos_options.general_purpose_interrupt, general_purpose_interrupt_handler); + microzig.cpu.interrupt.expect_handler(rtos_options.yield_interrupt, yield_interrupt_handler); + } + + const cs = microzig.interrupt.enter_critical_section(); + defer cs.leave(); + + rtos_state = .{ + .current_task = &main_task, + }; + if (rtos_options.paint_stack_byte) |paint_byte| { + @memset(&idle_stack, paint_byte); + } + make_ready(&idle_task); + + microzig.cpu.interrupt.map(rtos_options.cpu_interrupt.source(), rtos_options.yield_interrupt); + microzig.cpu.interrupt.set_type(rtos_options.yield_interrupt, .level); + microzig.cpu.interrupt.set_priority(rtos_options.yield_interrupt, .lowest); + microzig.cpu.interrupt.enable(rtos_options.yield_interrupt); + + // unit0 is already enabled as it is used by `hal.time`. + if (rtos_options.systimer_unit != .unit0) { + rtos_options.systimer_unit.apply(.enabled); + } + rtos_options.systimer_alarm.set_unit(rtos_options.systimer_unit); + rtos_options.systimer_alarm.set_mode(.target); + rtos_options.systimer_alarm.set_enabled(false); + rtos_options.systimer_alarm.set_interrupt_enabled(true); + + microzig.cpu.interrupt.map(rtos_options.systimer_alarm.interrupt_source(), rtos_options.general_purpose_interrupt); + microzig.cpu.interrupt.set_type(rtos_options.general_purpose_interrupt, .level); + microzig.cpu.interrupt.set_priority(rtos_options.general_purpose_interrupt, .lowest); + microzig.cpu.interrupt.enable(rtos_options.general_purpose_interrupt); +} + +// TODO: deinit + +fn idle() linksection(".ram_text") callconv(.naked) void { + // interrupts are initially disabled in new tasks + asm volatile ( + \\csrsi mstatus, 8 + \\1: + \\wfi + \\j 1b + ); +} + +pub fn get_current_task() *Task { + return rtos_state.current_task; +} + +pub const SpawnOptions = struct { + name: ?[]const u8 = null, + stack_size: usize = 4096, + priority: Priority = .lowest, +}; + +pub fn spawn( + gpa: std.mem.Allocator, + comptime function: anytype, + args: std.meta.ArgsTuple(@TypeOf(function)), + options: SpawnOptions, +) !*Task { + assert(options.priority != .idle); + + const Args = @TypeOf(args); + const args_align: std.mem.Alignment = comptime .fromByteUnits(@alignOf(Args)); + + const TypeErased = struct { + fn call() callconv(.c) void { + // interrupts are initially disabled in new tasks + microzig.cpu.interrupt.enable_interrupts(); + + const context_ptr: *const Args = + @ptrFromInt(args_align.forward(@intFromPtr(rtos_state.current_task) + @sizeOf(Task))); + @call(.auto, function, context_ptr.*); + if (@typeInfo(@TypeOf(function)).@"fn".return_type.? != noreturn) { + yield(.delete); + unreachable; + } + } + }; + + const alloc_align = comptime STACK_ALIGN.max(.of(Task)).max(args_align); + + const args_start = args_align.forward(@sizeOf(Task)); + const stack_start = STACK_ALIGN.forward(args_start + @sizeOf(Args)); + const stack_end = STACK_ALIGN.forward(stack_start + options.stack_size + EXTRA_STACK_SIZE); + + const alloc_size = stack_end; + const raw_alloc = try gpa.alignedAlloc(u8, alloc_align, alloc_size); + + const task: *Task = @ptrCast(@alignCast(raw_alloc)); + + const task_args: *Args = @ptrCast(@alignCast(raw_alloc[args_start..][0..@sizeOf(Args)])); + task_args.* = args; + + const stack: []u8 = raw_alloc[stack_start..stack_end]; + if (rtos_options.paint_stack_byte) |paint_byte| { + @memset(stack, paint_byte); + } + + task.* = .{ + .name = options.name, + .context = .{ + .sp = stack[stack.len..].ptr, + .pc = &TypeErased.call, + .fp = null, + }, + .stack = stack, + .priority = options.priority, + }; + + const cs = enter_critical_section(); + defer cs.leave(); + + make_ready(task); + + return task; +} + +/// Must execute inside a critical section. +pub fn make_ready(task: *Task) void { + switch (task.state) { + .ready, .running, .scheduled_for_deletion => return, + .none => {}, + .alarm_set => |_| { + rtos_state.timer_queue.remove(&task.node); + }, + .suspended => { + rtos_state.suspended_list.remove(&task.node); + }, + } + + task.state = .ready; + rtos_state.ready_queue.put(task); +} + +pub const YieldAction = union(enum) { + reschedule, + wait: struct { + timeout: ?TimerTicks = null, + }, + delete, +}; + +pub inline fn yield(action: YieldAction) void { + const cs = enter_critical_section(); + defer cs.leave(); + + const current_task, const next_task = yield_inner(action); + context_switch(¤t_task.context, &next_task.context); +} + +fn yield_inner(action: YieldAction) linksection(".ram_text") struct { *Task, *Task } { + assert(microzig.cpu.csr.mscratch.read_raw() == 0); + + const current_task = rtos_state.current_task; + switch (action) { + .reschedule => { + current_task.state = .ready; + rtos_state.ready_queue.put(current_task); + }, + .wait => |wait_action| { + assert(current_task != &idle_task); + + if (wait_action.timeout) |timeout| { + schedule_wake_at(current_task, timeout); + } else { + current_task.state = .suspended; + rtos_state.suspended_list.append(¤t_task.node); + } + }, + .delete => { + assert(current_task != &idle_task and current_task != &main_task); + + current_task.state = .scheduled_for_deletion; + rtos_state.scheduled_for_deletion_list.append(¤t_task.node); + }, + } + + const next_task: *Task = rtos_state.ready_queue.pop(null) orelse @panic("No task ready to run!"); + + next_task.state = .running; + rtos_state.current_task = next_task; + + return .{ current_task, next_task }; +} + +pub fn sleep(duration: time.Duration) void { + const timeout: TimerTicks = .after(duration); + while (!timeout.is_reached()) + yield(.{ .wait = .{ .timeout = timeout } }); +} + +inline fn context_switch(prev_context: *Context, next_context: *Context) void { + // Clobber all registers (except sp) to restore them after the context + // switch. + asm volatile ( + \\la a2, 1f + \\sw a2, 0(a0) # save return pc + \\sw sp, 4(a0) # save prev stack pointer + \\sw s0, 8(a0) # save prev frame pointer + \\ + \\lw a2, 0(a1) # load next pc + \\lw sp, 4(a1) # load next stack pointer + \\lw s0, 8(a1) # load next frame pointer + \\ + \\jr a2 # jump to next task + \\1: + \\ + : + : [prev_context] "{a0}" (prev_context), + [next_context] "{a1}" (next_context), + : .{ + .x1 = true, // ra + .x3 = true, + .x4 = true, + .x5 = true, + .x6 = true, + .x7 = true, + .x8 = true, + .x9 = true, + .x10 = true, + .x11 = true, + .x12 = true, + .x13 = true, + .x14 = true, + .x15 = true, + .x16 = true, + .x17 = true, + .x18 = true, + .x19 = true, + .x20 = true, + .x21 = true, + .x22 = true, + .x23 = true, + .x24 = true, + .x25 = true, + .x26 = true, + .x27 = true, + .x28 = true, + .x29 = true, + .x30 = true, + .x31 = true, + .memory = true, + }); +} + +pub fn yield_from_isr() void { + rtos_options.cpu_interrupt.set_pending(true); +} + +pub fn is_a_higher_priority_task_ready() bool { + return if (rtos_state.ready_queue.peek_top()) |top_ready_task| + @intFromEnum(top_ready_task.priority) > @intFromEnum(rtos_state.current_task.priority) + else + false; +} + +pub const yield_interrupt_handler: microzig.cpu.InterruptHandler = .{ + .naked = struct { + pub fn handler_fn() linksection(".ram_vectors") callconv(.naked) void { + comptime { + assert(@sizeOf(Context) == 3 * @sizeOf(usize)); + } + + asm volatile ( + \\ + \\addi sp, sp, -32*4 + \\ + \\sw ra, 0*4(sp) + \\sw t0, 1*4(sp) + \\sw t1, 2*4(sp) + \\sw t2, 3*4(sp) + \\sw t3, 4*4(sp) + \\sw t4, 5*4(sp) + \\sw t5, 6*4(sp) + \\sw t6, 7*4(sp) + \\sw a0, 8*4(sp) + \\sw a1, 9*4(sp) + \\sw a2, 10*4(sp) + \\sw a3, 11*4(sp) + \\sw a4, 12*4(sp) + \\sw a5, 13*4(sp) + \\sw a6, 14*4(sp) + \\sw a7, 15*4(sp) + // s0 is saved in context + \\sw s1, 16*4(sp) + \\sw s2, 17*4(sp) + \\sw s3, 18*4(sp) + \\sw s4, 19*4(sp) + \\sw s5, 20*4(sp) + \\sw s6, 21*4(sp) + \\sw s7, 22*4(sp) + \\sw s8, 23*4(sp) + \\sw s9, 24*4(sp) + \\sw s10, 25*4(sp) + \\sw s11, 26*4(sp) + \\sw gp, 27*4(sp) + \\sw tp, 28*4(sp) + \\ + \\csrr a0, mepc + \\sw a0, 29*4(sp) + \\ + \\csrr a0, mstatus + \\sw a0, 30*4(sp) + \\ + // save sp for later + \\mv a2, sp + \\ + // use the interrupt stack in this call to minimize task stack size + // NOTE: mscratch doesn't need to be zeroed because this can't be + // interrupted by a higher priority interrupt + \\la sp, %[interrupt_stack_top] + \\ + // allocate `Context` struct and save context + \\addi sp, sp, -16 + \\la a1, 1f + \\sw a1, 0(sp) + \\sw a2, 4(sp) + \\sw s0, 8(sp) + \\ + // first parameter is a pointer to context + \\mv a0, sp + \\jal %[schedule_in_isr] + \\ + // load next task context + \\lw a1, 0(sp) + \\lw a2, 4(sp) + \\lw s0, 8(sp) + // change sp to the new task + \\mv sp, a2 + \\ + // if the next task program counter is equal to 1f's location + // just jump to it (ie. the task forcefully yielded). + \\la a0, 1f + \\beq a1, a0, 1f + \\ + // ensure interrupts are disabled after mret (when a normal + // context switch occured) + \\li a0, 0x80 + \\csrc mstatus, a0 + \\ + // jump to new task + \\csrw mepc, a1 + \\mret + \\ + \\1: + \\ + \\lw a0, 30*4(sp) + \\csrw mstatus, a0 + \\ + \\lw a0, 29*4(sp) + \\csrw mepc, a0 + \\ + \\lw ra, 0*4(sp) + \\lw t0, 1*4(sp) + \\lw t1, 2*4(sp) + \\lw t2, 3*4(sp) + \\lw t3, 4*4(sp) + \\lw t4, 5*4(sp) + \\lw t5, 6*4(sp) + \\lw t6, 7*4(sp) + \\lw a0, 8*4(sp) + \\lw a1, 9*4(sp) + \\lw a2, 10*4(sp) + \\lw a3, 11*4(sp) + \\lw a4, 12*4(sp) + \\lw a5, 13*4(sp) + \\lw a6, 14*4(sp) + \\lw a7, 15*4(sp) + \\lw s1, 16*4(sp) + \\lw s2, 17*4(sp) + \\lw s3, 18*4(sp) + \\lw s4, 19*4(sp) + \\lw s5, 20*4(sp) + \\lw s6, 21*4(sp) + \\lw s7, 22*4(sp) + \\lw s8, 23*4(sp) + \\lw s9, 24*4(sp) + \\lw s10, 25*4(sp) + \\lw s11, 26*4(sp) + \\lw gp, 27*4(sp) + \\lw tp, 28*4(sp) + \\ + \\addi sp, sp, 32*4 + \\mret + : + : [schedule_in_isr] "i" (&schedule_in_isr), + [interrupt_stack_top] "i" (microzig.cpu.interrupt_stack[microzig.cpu.interrupt_stack.len..].ptr), + ); + } + }.handler_fn, +}; + +// Can't be preempted by a higher priority interrupt. +fn schedule_in_isr(context: *Context) linksection(".ram_vectors") callconv(.c) void { + rtos_options.cpu_interrupt.set_pending(false); + + const current_task = rtos_state.current_task; + const ready_task = rtos_state.ready_queue.pop(rtos_state.current_task.priority) orelse return; + + // swap contexts + current_task.context = context.*; + context.* = ready_task.context; + + current_task.state = .ready; + rtos_state.ready_queue.put(current_task); + + ready_task.state = .running; + rtos_state.current_task = ready_task; +} + +pub const general_purpose_interrupt_handler: microzig.cpu.InterruptHandler = .{ .c = struct { + pub fn handler_fn(_: *TrapFrame) linksection(".ram_text") callconv(.c) void { + const cs = enter_critical_section(); + defer cs.leave(); + + var status: microzig.cpu.interrupt.Status = .init(); + if (status.is_set(rtos_options.systimer_alarm.interrupt_source())) { + rtos_options.systimer_alarm.clear_interrupt(); + + sweep_timer_queue_for_timeouts(); + } + + if (is_a_higher_priority_task_ready()) { + yield_from_isr(); + } + } +}.handler_fn }; + +/// Must execute inside a critical section. +fn schedule_wake_at(sleeping_task: *Task, ticks: TimerTicks) void { + sleeping_task.state = .{ .alarm_set = ticks }; + + var maybe_node = rtos_state.timer_queue.first; + while (maybe_node) |node| : (maybe_node = node.next) { + const task: *Task = @alignCast(@fieldParentPtr("node", node)); + if (ticks.is_before(task.state.alarm_set)) { + rtos_state.timer_queue.insertBefore(&task.node, &sleeping_task.node); + break; + } + } else { + rtos_state.timer_queue.append(&sleeping_task.node); + } + + // If we updated the first element of the list, it means that we have to + // reschedule the timer + if (rtos_state.timer_queue.first == &sleeping_task.node) { + rtos_options.systimer_alarm.set_target(@intFromEnum(ticks)); + rtos_options.systimer_alarm.set_enabled(true); + + if (ticks.is_reached()) + sweep_timer_queue_for_timeouts(); + } +} + +fn sweep_timer_queue_for_timeouts() void { + while (rtos_state.timer_queue.popFirst()) |node| { + const task: *Task = @alignCast(@fieldParentPtr("node", node)); + if (!task.state.alarm_set.is_reached()) { + rtos_state.timer_queue.prepend(&task.node); + break; + } + task.state = .ready; + rtos_state.ready_queue.put(task); + } + + if (rtos_state.timer_queue.first) |node| { + const task: *Task = @alignCast(@fieldParentPtr("node", node)); + rtos_options.systimer_alarm.set_target(@intFromEnum(task.state.alarm_set)); + rtos_options.systimer_alarm.set_enabled(true); + } else { + rtos_options.systimer_alarm.set_enabled(false); + } +} + +pub fn log_tasks_info() void { + const cs = microzig.interrupt.enter_critical_section(); + defer cs.leave(); + + log_task_info(get_current_task()); + + const list: []const ?*std.DoublyLinkedList.Node = &.{ + rtos_state.ready_queue.inner.first, + rtos_state.timer_queue.first, + rtos_state.suspended_list.first, + rtos_state.scheduled_for_deletion_list.first, + }; + for (list) |first| { + var it: ?*std.DoublyLinkedList.Node = first; + while (it) |node| : (it = node.next) { + const task: *Task = @alignCast(@fieldParentPtr("node", node)); + log_task_info(task); + } + } +} + +fn log_task_info(task: *Task) void { + if (rtos_options.paint_stack_byte) |paint_byte| { + const stack_usage = for (task.stack, 0..) |byte, i| { + if (byte != paint_byte) { + break task.stack.len - i; + } + } else task.stack.len; + + log.debug("task {?s} with prio {} in state {t} uses {} bytes out of {} for stack", .{ + task.name, + @intFromEnum(task.priority), + task.state, + stack_usage, + task.stack.len, + }); + } else { + log.debug("task {?s} with prio {} in state {t}", .{ + task.name, + @intFromEnum(task.priority), + task.state, + }); + } +} + +pub const Task = struct { + name: ?[]const u8 = null, + + context: Context, + stack: []u8, + priority: Priority, + + /// What is the deal with this task right now? + state: State = .none, + + /// Node used for rtos internal lists. + node: std.DoublyLinkedList.Node = .{}, + + /// Task specific semaphore (required by the wifi driver) + semaphore: Semaphore = .init(0, 1), + + pub const State = union(enum) { + none, + ready, + running, + alarm_set: TimerTicks, + suspended, + scheduled_for_deletion, + }; +}; + +pub const Context = extern struct { + pc: ?*const anyopaque, + sp: ?*const anyopaque, + fp: ?*const anyopaque, + + pub fn format( + self: Context, + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { + try writer.print(".{{ .pc = 0x{x}, .sp = 0x{x}, .fp = 0x{x} }}", .{ + @intFromPtr(self.pc), + @intFromPtr(self.sp), + @intFromPtr(self.fp), + }); + } +}; + +pub const ReadyPriorityQueue = struct { + inner: std.DoublyLinkedList = .{}, + + pub fn peek_top(pq: *ReadyPriorityQueue) ?*Task { + if (pq.inner.first) |first_node| { + return @alignCast(@fieldParentPtr("node", first_node)); + } else { + return null; + } + } + + pub fn pop(pq: *ReadyPriorityQueue, maybe_more_than_prio: ?Priority) ?*Task { + if (pq.peek_top()) |task| { + if (maybe_more_than_prio) |more_than_prio| { + if (@intFromEnum(task.priority) <= @intFromEnum(more_than_prio)) { + return null; + } + } + pq.inner.remove(&task.node); + return task; + } + return null; + } + + pub fn put(pq: *ReadyPriorityQueue, new_task: *Task) void { + var maybe_node = pq.inner.first; + while (maybe_node) |node| : (maybe_node = node.next) { + const task: *Task = @alignCast(@fieldParentPtr("node", node)); + if (@intFromEnum(new_task.priority) > @intFromEnum(task.priority)) { + pq.inner.insertBefore(node, &new_task.node); + break; + } + } else { + pq.inner.append(&new_task.node); + } + } +}; + +pub const TimerTicks = enum(u52) { + _, + + pub fn now() TimerTicks { + return @enumFromInt(rtos_options.systimer_unit.read()); + } + + pub fn after(duration: time.Duration) TimerTicks { + return TimerTicks.now().add_duration(duration); + } + + pub fn is_reached(ticks: TimerTicks) bool { + return ticks.is_before(.now()); + } + + pub fn is_before(a: TimerTicks, b: TimerTicks) bool { + const _a = @intFromEnum(a); + const _b = @intFromEnum(b); + return _a < _b or _b -% _a < _a; + } + + pub fn add_duration(ticks: TimerTicks, duration: time.Duration) TimerTicks { + return @enumFromInt(@intFromEnum(ticks) +% @as(u52, @intCast(duration.to_us())) * systimer.ticks_per_us()); + } +}; + +pub const TimeoutError = error{Timeout}; + +pub const PriorityWaitQueue = struct { + list: std.DoublyLinkedList = .{}, + + pub const Waiter = struct { + task: *Task, + priority: Priority, + node: std.DoublyLinkedList.Node = .{}, + }; + + /// Must execute inside a critical section. + pub fn wake_one(q: *PriorityWaitQueue) void { + if (q.list.first) |first_node| { + const waiter: *Waiter = @alignCast(@fieldParentPtr("node", first_node)); + make_ready(waiter.task); + } + } + + /// Must execute inside a critical section. + pub fn wake_all(q: *PriorityWaitQueue) void { + while (q.list.popFirst()) |current_node| { + const current_waiter: *Waiter = @alignCast(@fieldParentPtr("node", current_node)); + make_ready(current_waiter.task); + } + } + + /// Must execute inside a critical section. + pub fn wait(q: *PriorityWaitQueue, task: *Task, maybe_timeout: ?TimerTicks) void { + var waiter: Waiter = .{ + .task = task, + .priority = task.priority, + }; + + var it = q.list.first; + while (it) |current_node| : (it = current_node.next) { + const current_waiter: *Waiter = @alignCast(@fieldParentPtr("node", current_node)); + if (@intFromEnum(waiter.priority) > @intFromEnum(current_waiter.priority)) { + q.list.insertBefore(¤t_waiter.node, &waiter.node); + break; + } + } else { + q.list.append(&waiter.node); + } + + yield(.{ .wait = .{ + .timeout = maybe_timeout, + } }); + + q.list.remove(&waiter.node); + } +}; + +pub const Mutex = struct { + locked: ?*Task = null, + prev_priority: ?Priority = null, + wait_queue: PriorityWaitQueue = .{}, + + pub fn lock(mutex: *Mutex) void { + mutex.lock_with_timeout(null) catch unreachable; + } + + pub fn lock_with_timeout(mutex: *Mutex, maybe_timeout: ?time.Duration) TimeoutError!void { + const cs = enter_critical_section(); + defer cs.leave(); + + const current_task = get_current_task(); + + const maybe_timeout_ticks: ?TimerTicks = if (maybe_timeout) |timeout| + .after(timeout) + else + null; + + assert(mutex.locked != current_task); + + while (mutex.locked) |owning_task| { + if (maybe_timeout_ticks) |timeout_ticks| + if (timeout_ticks.is_reached()) + return error.Timeout; + + // Owning task inherits the priority of the current task if it the + // current task has a bigger priority. + if (@intFromEnum(current_task.priority) > @intFromEnum(owning_task.priority)) { + if (mutex.prev_priority == null) + mutex.prev_priority = owning_task.priority; + owning_task.priority = current_task.priority; + make_ready(owning_task); + } + + mutex.wait_queue.wait(current_task, maybe_timeout_ticks); + } + + mutex.locked = current_task; + } + + pub fn unlock(mutex: *Mutex) void { + const cs = enter_critical_section(); + defer cs.leave(); + + mutex.unlock_impl(); + } + + fn unlock_impl(mutex: *Mutex) void { + const owning_task = mutex.locked.?; + + // Restore the priority of the task + if (mutex.prev_priority) |prev_priority| { + owning_task.priority = prev_priority; + mutex.prev_priority = null; + } + + mutex.locked = null; + mutex.wait_queue.wake_one(); + } +}; + +pub const Condition = struct { + wait_queue: PriorityWaitQueue = .{}, + + pub fn wait(cond: *Condition, mutex: *Mutex) void { + { + const cs = enter_critical_section(); + defer cs.leave(); + mutex.unlock_impl(); + cond.wait_queue.wait(get_current_task(), null); + } + + mutex.lock(); + } + + pub fn signal(cond: *Condition) void { + const cs = enter_critical_section(); + defer cs.leave(); + cond.wait_queue.wake_one(); + } + + pub fn broadcast(cond: *Condition) void { + const cs = enter_critical_section(); + defer cs.leave(); + cond.wait_queue.wake_all(); + } +}; + +pub const Semaphore = struct { + current_value: u32, + max_value: u32, + wait_queue: PriorityWaitQueue = .{}, + + pub fn init(initial_value: u32, max_value: u32) Semaphore { + assert(initial_value <= max_value); + assert(max_value > 0); + + return .{ + .current_value = initial_value, + .max_value = max_value, + }; + } + + pub fn take(sem: *Semaphore) void { + sem.take_with_timeout(null) catch unreachable; + } + + pub fn take_with_timeout(sem: *Semaphore, maybe_timeout: ?time.Duration) TimeoutError!void { + const cs = enter_critical_section(); + defer cs.leave(); + + const maybe_timeout_ticks: ?TimerTicks = if (maybe_timeout) |timeout| + .after(timeout) + else + null; + + while (sem.current_value <= 0) { + if (maybe_timeout_ticks) |timeout_ticks| + if (timeout_ticks.is_reached()) + return error.Timeout; + + sem.wait_queue.wait(rtos_state.current_task, maybe_timeout_ticks); + } + + sem.current_value -= 1; + } + + pub fn give(sem: *Semaphore) void { + const cs = enter_critical_section(); + defer cs.leave(); + + sem.current_value += 1; + if (sem.current_value > sem.max_value) { + sem.current_value = sem.max_value; + } else { + sem.wait_queue.wake_one(); + } + } +}; + +pub const TypeErasedQueue = struct { + buffer: []u8, + start: usize, + len: usize, + + putters: PriorityWaitQueue, + getters: PriorityWaitQueue, + + pub fn init(buffer: []u8) TypeErasedQueue { + assert(buffer.len != 0); // buffer len must be greater than 0 + return .{ + .buffer = buffer, + .start = 0, + .len = 0, + .putters = .{}, + .getters = .{}, + }; + } + + pub fn put( + q: *TypeErasedQueue, + elements: []const u8, + min: usize, + maybe_timeout: ?time.Duration, + ) usize { + assert(elements.len >= min); + if (elements.len == 0) return 0; + + const maybe_timeout_ticks: ?TimerTicks = if (maybe_timeout) |timeout| + .after(timeout) + else + null; + + var n: usize = 0; + + const cs = enter_critical_section(); + defer cs.leave(); + + while (true) { + n += q.put_non_blocking_from_cs(elements[n..]); + if (n >= min) return n; + + if (maybe_timeout_ticks) |timeout_ticks| + if (timeout_ticks.is_reached()) + return n; + + q.putters.wait(rtos_state.current_task, maybe_timeout_ticks); + } + } + + pub fn put_non_blocking(q: *TypeErasedQueue, elements: []const u8) usize { + const cs = enter_critical_section(); + defer cs.leave(); + + return q.put_non_blocking_from_cs(elements); + } + + fn put_non_blocking_from_cs(q: *TypeErasedQueue, elements: []const u8) usize { + var n: usize = 0; + while (q.puttable_slice()) |slice| { + const copy_len = @min(slice.len, elements.len - n); + assert(copy_len > 0); + @memcpy(slice[0..copy_len], elements[n..][0..copy_len]); + q.len += copy_len; + n += copy_len; + if (n == elements.len) break; + } + if (n > 0) q.getters.wake_one(); + return n; + } + + fn puttable_slice(q: *const TypeErasedQueue) ?[]u8 { + const unwrapped_index = q.start + q.len; + const wrapped_index, const overflow = @subWithOverflow(unwrapped_index, q.buffer.len); + const slice = switch (overflow) { + 1 => q.buffer[unwrapped_index..], + 0 => q.buffer[wrapped_index..q.start], + }; + return if (slice.len > 0) slice else null; + } + + pub fn get( + q: *TypeErasedQueue, + buffer: []u8, + min: usize, + maybe_timeout: ?time.Duration, + ) usize { + assert(buffer.len >= min); + if (buffer.len == 0) return 0; + + const maybe_timeout_ticks: ?TimerTicks = if (maybe_timeout) |timeout| + .after(timeout) + else + null; + + var n: usize = 0; + + const cs = enter_critical_section(); + defer cs.leave(); + + while (true) { + n += q.get_non_blocking_from_cs(buffer[n..]); + if (n >= min) return n; + + if (maybe_timeout_ticks) |timeout_ticks| + if (timeout_ticks.is_reached()) + return n; + + q.getters.wait(rtos_state.current_task, maybe_timeout_ticks); + } + } + + pub fn get_non_blocking(q: *TypeErasedQueue, buffer: []u8) usize { + const cs = enter_critical_section(); + defer cs.leave(); + + return q.get_non_blocking_from_cs(buffer); + } + + fn get_non_blocking_from_cs(q: *TypeErasedQueue, buffer: []u8) usize { + var n: usize = 0; + while (q.gettable_slice()) |slice| { + const copy_len = @min(slice.len, buffer.len - n); + assert(copy_len > 0); + @memcpy(buffer[n..][0..copy_len], slice[0..copy_len]); + q.start += copy_len; + if (q.buffer.len - q.start == 0) q.start = 0; + q.len -= copy_len; + n += copy_len; + if (n == buffer.len) break; + } + if (n > 0) q.putters.wake_one(); + return n; + } + + fn gettable_slice(q: *const TypeErasedQueue) ?[]const u8 { + const overlong_slice = q.buffer[q.start..]; + const slice = overlong_slice[0..@min(overlong_slice.len, q.len)]; + return if (slice.len > 0) slice else null; + } +}; + +pub fn Queue(Elem: type) type { + return struct { + const Self = @This(); + + type_erased: TypeErasedQueue, + + pub fn init(buffer: []Elem) Self { + return .{ .type_erased = .init(@ptrCast(buffer)) }; + } + + pub fn put(q: *Self, elements: []const Elem, min: usize, timeout: ?time.Duration) usize { + return @divExact(q.type_erased.put(@ptrCast(elements), min * @sizeOf(Elem), timeout), @sizeOf(Elem)); + } + + pub fn put_all(q: *Self, elements: []const Elem, timeout: ?time.Duration) TimeoutError!void { + if (q.put(elements, elements.len, timeout) != elements.len) + return error.Timeout; + } + + pub fn put_one(q: *Self, item: Elem, timeout: ?time.Duration) TimeoutError!void { + if (q.put(&.{item}, 1, timeout) != 1) + return error.Timeout; + } + + pub fn put_non_blocking(q: *Self, elements: []const Elem) usize { + return @divExact(q.type_erased.put_non_blocking(@ptrCast(elements)), @sizeOf(Elem)); + } + + pub fn put_one_non_blocking(q: *Self, item: Elem) bool { + return q.put_non_blocking(@ptrCast(&item)) == 1; + } + + pub fn get(q: *Self, buffer: []Elem, target: usize, timeout: ?time.Duration) usize { + return @divExact(q.type_erased.get(@ptrCast(buffer), target * @sizeOf(Elem), timeout), @sizeOf(Elem)); + } + + pub fn get_one(q: *Self, timeout: ?time.Duration) TimeoutError!Elem { + var buf: [1]Elem = undefined; + if (q.get(&buf, 1, timeout) != 1) + return error.Timeout; + return buf[0]; + } + + pub fn get_one_non_blocking(q: *Self) ?Elem { + var buf: [1]Elem = undefined; + if (q.type_erased.get_non_blocking(@ptrCast(&buf)) == 1) { + return buf[0]; + } else { + return null; + } + } + + pub fn capacity(q: *const Self) usize { + return @divExact(q.type_erased.buffer.len, @sizeOf(Elem)); + } + }; +} diff --git a/port/espressif/esp/src/hal/system.zig b/port/espressif/esp/src/hal/system.zig index b31221d1d..a1643d8ca 100644 --- a/port/espressif/esp/src/hal/system.zig +++ b/port/espressif/esp/src/hal/system.zig @@ -124,3 +124,31 @@ pub fn enable_clocks_and_release_reset(mask: PeripheralMask) void { clocks_enable_set(mask); peripheral_reset_clear(mask); } + +pub const CPU_Interrupt = enum { + cpu_interrupt_0, + cpu_interrupt_1, + cpu_interrupt_2, + cpu_interrupt_3, + + pub fn source(cpu_interrupt: CPU_Interrupt) microzig.cpu.interrupt.Source { + return switch (cpu_interrupt) { + .cpu_interrupt_0 => .from_cpu_intr0, + .cpu_interrupt_1 => .from_cpu_intr1, + .cpu_interrupt_2 => .from_cpu_intr2, + .cpu_interrupt_3 => .from_cpu_intr3, + }; + } + + pub fn set_pending(cpu_interrupt: CPU_Interrupt, enabled: bool) void { + const regs: @TypeOf(&SYSTEM.CPU_INTR_FROM_CPU_0) = switch (cpu_interrupt) { + .cpu_interrupt_0 => @ptrCast(&SYSTEM.CPU_INTR_FROM_CPU_0), + .cpu_interrupt_1 => @ptrCast(&SYSTEM.CPU_INTR_FROM_CPU_1), + .cpu_interrupt_2 => @ptrCast(&SYSTEM.CPU_INTR_FROM_CPU_2), + .cpu_interrupt_3 => @ptrCast(&SYSTEM.CPU_INTR_FROM_CPU_3), + }; + regs.write(.{ + .CPU_INTR_FROM_CPU_0 = @intFromBool(enabled), + }); + } +}; diff --git a/port/espressif/esp/src/hal/systimer.zig b/port/espressif/esp/src/hal/systimer.zig index 7782e0b69..6c3691adc 100644 --- a/port/espressif/esp/src/hal/systimer.zig +++ b/port/espressif/esp/src/hal/systimer.zig @@ -4,7 +4,7 @@ const SYSTIMER = microzig.chip.peripherals.SYSTIMER; const system = @import("system.zig"); -pub fn ticks_per_us() u64 { +pub fn ticks_per_us() u52 { return 16; } @@ -69,7 +69,7 @@ pub const Unit = enum(u1) { } } - pub fn write(self: Unit, value: u64) void { + pub fn write(self: Unit, value: u52) void { const LOAD: @TypeOf(&SYSTIMER.UNIT0_LOAD) = switch (self) { .unit0 => &SYSTIMER.UNIT0_LOAD, .unit1 => @ptrCast(&SYSTIMER.UNIT1_LOAD), @@ -83,12 +83,12 @@ pub const Unit = enum(u1) { .unit1 => @ptrCast(&SYSTIMER.UNIT1_VALUE_HI), }; - LO.write(.{ .TIMER_UNIT0_LOAD_LO = value & 0xffff_ffff }); - HI.write(.{ .TIMER_UNIT0_LOAD_HI = value >> 32 }); + LO.write(.{ .TIMER_UNIT0_LOAD_LO = @truncate(value) }); + HI.write(.{ .TIMER_UNIT0_LOAD_HI = @truncate(value >> 32) }); LOAD.write(.{ .TIMER_UNIT0_LOAD = 1 }); } - pub fn read(self: Unit) u64 { + pub fn read(self: Unit) u52 { const OP: @TypeOf(&SYSTIMER.UNIT0_OP) = switch (self) { .unit0 => &SYSTIMER.UNIT0_OP, .unit1 => @ptrCast(&SYSTIMER.UNIT1_OP), @@ -115,7 +115,7 @@ pub const Unit = enum(u1) { const hi = HI.read().TIMER_UNIT0_VALUE_HI; lo_start = LO.read().TIMER_UNIT0_VALUE_LO; - if (lo_start == lo) return (@as(u64, hi) << 32) | lo; + if (lo_start == lo) return (@as(u52, hi) << 32) | lo; } } }; @@ -183,6 +183,14 @@ pub const Alarm = enum(u2) { }); } + pub fn interrupt_source(self: Alarm) microzig.cpu.interrupt.Source { + return switch (self) { + .alarm0 => .systimer_target0, + .alarm1 => .systimer_target1, + .alarm2 => .systimer_target2, + }; + } + pub fn set_interrupt_enabled(self: Alarm, enable: bool) void { switch (self) { .alarm0 => SYSTIMER.INT_ENA.modify(.{ .TARGET0_INT_ENA = @intFromBool(enable) }), diff --git a/tools/printer/src/root.zig b/tools/printer/src/root.zig index 8ef816067..6221f9585 100644 --- a/tools/printer/src/root.zig +++ b/tools/printer/src/root.zig @@ -97,7 +97,7 @@ fn output_source_line( out_tty_config: std.io.tty.Config, src_loc: DebugInfo.ResolvedSourceLocation, ) !void { - var dir = try std.fs.cwd().openDir(src_loc.dir_path, .{}); + var dir = std.fs.cwd().openDir(src_loc.dir_path, .{}) catch return; defer dir.close(); const file = dir.openFile(src_loc.file_path, .{}) catch return;