diff --git a/examples/microchip/atmega/build.zig b/examples/microchip/atmega/build.zig index 815869f1f..959016651 100644 --- a/examples/microchip/atmega/build.zig +++ b/examples/microchip/atmega/build.zig @@ -22,6 +22,7 @@ pub fn build(b: *std.Build) void { .{ .target = mb.ports.atmega.boards.arduino.nano, .name = "arduino-pro_mini_5v_uart_echo", .file = "src/uart_echo.zig" }, .{ .target = mb.ports.atmega.boards.arduino.uno_rev3, .name = "arduino-uno_uart_echo", .file = "src/uart_echo.zig" }, .{ .target = mb.ports.atmega.boards.emakefun.rf_nano_v3, .name = "rf-nano-v3_uart_echo", .file = "src/uart_echo.zig" }, + .{ .target = mb.ports.atmega.boards.emakefun.rf_nano_v3, .name = "rf-nano-v3_spi_nrf24", .file = "src/spi_nrf24.zig" }, }; for (available_examples) |example| { diff --git a/examples/microchip/atmega/src/spi_nrf24.zig b/examples/microchip/atmega/src/spi_nrf24.zig new file mode 100644 index 000000000..8fbc9a8e6 --- /dev/null +++ b/examples/microchip/atmega/src/spi_nrf24.zig @@ -0,0 +1,64 @@ +//! Reads the STATUS and CONFIG registers of an nRF24L01+ over SPI and prints +//! them over USART0 (115200 8N1) each time a key is pressed. After the +//! radio's power-on reset it reports STATUS=0x0E CONFIG=0x08. +//! +//! Wiring as on the Emakefun RF-Nano V3.0, which has the radio on board: CS on +//! D8 (PB0) and the hardware SPI pins (D11, D12, D13). + +const microzig = @import("microzig"); +const hal = microzig.hal; +const board = microzig.board; + +pub const panic = microzig.panic; +pub const std_options = microzig.std_options(.{}); + +comptime { + _ = microzig.export_startup(); +} + +const cs_pin = hal.gpio.pin(.b, 0); +const spi = hal.spi.instance.SPI0; + +// nRF24L01+ command and register read here (datasheet §8.3.1 and §9). +const r_register = 0x00; +const config_register = 0x00; + +/// Clocked out to receive a byte; the radio ignores it. +const dummy_byte = 0xFF; + +pub fn main() void { + cs_pin.put(1); // deselect the radio before CS becomes an output + cs_pin.set_direction(.output); + + board.uart_setup.apply(.{ .baud_rate = 115_200 }); + board.spi_setup.apply(.{ .baud_rate = 1_000_000 }); + + // Reading only after a key press leaves time for the radio's 100 ms + // power-on reset, unless a key arrives right after power-up. + hal.uart.write_blocking("Press a key to read the radio registers.\r\n"); + + while (true) { + var key: u8 = undefined; + hal.uart.read_byte_blocking(&key) catch continue; + + // R_REGISTER for CONFIG (0x00). The radio shifts out STATUS while it + // receives the command byte, then CONFIG. + var rx: [2]u8 = undefined; + cs_pin.put(0); + spi.transceive_blocking(&.{ r_register | config_register, dummy_byte }, &rx); + cs_pin.put(1); + + hal.uart.write_blocking("STATUS="); + write_hex(rx[0]); + hal.uart.write_blocking(" CONFIG="); + write_hex(rx[1]); + hal.uart.write_blocking("\r\n"); + } +} + +fn write_hex(byte: u8) void { + hal.uart.write_blocking("0x"); + for ([_]u8{ byte >> 4, byte & 0x0F }) |nibble| { + hal.uart.write_byte_blocking(if (nibble < 10) '0' + nibble else 'A' - 10 + nibble); + } +} diff --git a/port/microchip/atmega/build.zig b/port/microchip/atmega/build.zig index 9680a7995..fdf0dc5a1 100644 --- a/port/microchip/atmega/build.zig +++ b/port/microchip/atmega/build.zig @@ -140,12 +140,13 @@ pub fn init(dep: *std.Build.Dependency) ?Self { pub fn build(b: *std.Build) void { const test_step = b.step("test", "Run platform agnostic unit tests"); - const uart_tests = b.addTest(.{ - .root_module = b.createModule(.{ - .root_source_file = b.path("src/hals/atmega328p/uart.zig"), - .target = b.graph.host, - }), - }); - - test_step.dependOn(&b.addRunArtifact(uart_tests).step); + for ([_][]const u8{ "uart", "spi" }) |name| { + const tests = b.addTest(.{ + .root_module = b.createModule(.{ + .root_source_file = b.path(b.fmt("src/hals/atmega328p/{s}.zig", .{name})), + .target = b.graph.host, + }), + }); + test_step.dependOn(&b.addRunArtifact(tests).step); + } } diff --git a/port/microchip/atmega/src/boards/arduino_nano.zig b/port/microchip/atmega/src/boards/arduino_nano.zig index 55a799bb4..14fd2c34e 100644 --- a/port/microchip/atmega/src/boards/arduino_nano.zig +++ b/port/microchip/atmega/src/boards/arduino_nano.zig @@ -8,6 +8,10 @@ pub const clock_frequencies = .{ /// USART0 on PD0 (RX) / PD1 (TX), wired to the USB serial chip. pub const uart_setup: hal.uart.Setup = .{ .cpu_frequency = clock_frequencies.cpu }; +/// SPI on PB5 (SCK), PB3 (MOSI) and PB4 (MISO). PB2 (D10) is set as an +/// output to keep the SPI in host mode. +pub const spi_setup: hal.spi.Setup = .{ .cpu_frequency = clock_frequencies.cpu }; + pub const pin_map = .{ // Port A .D0 = "PD0", diff --git a/port/microchip/atmega/src/boards/arduino_pro_mini_3v3.zig b/port/microchip/atmega/src/boards/arduino_pro_mini_3v3.zig index 46706b869..6c08b2284 100644 --- a/port/microchip/atmega/src/boards/arduino_pro_mini_3v3.zig +++ b/port/microchip/atmega/src/boards/arduino_pro_mini_3v3.zig @@ -8,6 +8,10 @@ pub const clock_frequencies = .{ /// USART0 on PD0 (RX) / PD1 (TX), exposed on programming header pub const uart_setup: hal.uart.Setup = .{ .cpu_frequency = clock_frequencies.cpu }; +/// SPI on PB5 (SCK), PB3 (MOSI) and PB4 (MISO). PB2 (D10) is set as an +/// output to keep the SPI in host mode. +pub const spi_setup: hal.spi.Setup = .{ .cpu_frequency = clock_frequencies.cpu }; + pub const pin_map = .{ // Port A .D0 = "PD0", diff --git a/port/microchip/atmega/src/boards/arduino_pro_mini_5v.zig b/port/microchip/atmega/src/boards/arduino_pro_mini_5v.zig index a4d96efc5..acca28d02 100644 --- a/port/microchip/atmega/src/boards/arduino_pro_mini_5v.zig +++ b/port/microchip/atmega/src/boards/arduino_pro_mini_5v.zig @@ -8,6 +8,10 @@ pub const clock_frequencies = .{ /// USART0 on PD0 (RX) / PD1 (TX), exposed on programming header pub const uart_setup: hal.uart.Setup = .{ .cpu_frequency = clock_frequencies.cpu }; +/// SPI on PB5 (SCK), PB3 (MOSI) and PB4 (MISO). PB2 (D10) is set as an +/// output to keep the SPI in host mode. +pub const spi_setup: hal.spi.Setup = .{ .cpu_frequency = clock_frequencies.cpu }; + pub const pin_map = .{ // Port A .D0 = "PD0", diff --git a/port/microchip/atmega/src/boards/arduino_uno.zig b/port/microchip/atmega/src/boards/arduino_uno.zig index 6ae78b0d4..5fa96c29d 100644 --- a/port/microchip/atmega/src/boards/arduino_uno.zig +++ b/port/microchip/atmega/src/boards/arduino_uno.zig @@ -8,6 +8,10 @@ pub const clock_frequencies = .{ /// USART0 on PD0 (RX) / PD1 (TX), wired to the USB serial chip. pub const uart_setup: hal.uart.Setup = .{ .cpu_frequency = clock_frequencies.cpu }; +/// SPI on PB5 (SCK), PB3 (MOSI) and PB4 (MISO). PB2 (D10) is set as an +/// output to keep the SPI in host mode. +pub const spi_setup: hal.spi.Setup = .{ .cpu_frequency = clock_frequencies.cpu }; + pub const pin_map = .{ // Port D .D0 = "PD0", diff --git a/port/microchip/atmega/src/boards/rf_nano_v3.zig b/port/microchip/atmega/src/boards/rf_nano_v3.zig index bf1805afa..33f22d2cb 100644 --- a/port/microchip/atmega/src/boards/rf_nano_v3.zig +++ b/port/microchip/atmega/src/boards/rf_nano_v3.zig @@ -1,7 +1,7 @@ //! Emakefun RF-Nano V3.0: Arduino Nano V3.0 compatible board with an onboard //! nRF24L01+ (or Si24R1) wired to the hardware SPI bus. //! -//! - Radio: CE = D7 (PD7), CS = D8 (PB0), SCK = D13 (PB5), HODI = D11 (PB3), HIDO = D12 (PB4). +//! - Radio: CE = D7 (PD7), CS = D8 (PB0), SCK = D13 (PB5), MOSI = D11 (PB3), MISO = D12 (PB4). //! - The radio IRQ pin is not connected; poll the STATUS register instead. //! - The onboard LED shares D13 (PB5) with SCK, so it flickers with SPI traffic. //! - D7, D8, D11, D12 and D13 are used by the radio. @@ -18,6 +18,10 @@ pub const clock_frequencies = .{ /// USART0 on PD0 (RX) / PD1 (TX), wired to the USB serial chip. pub const uart_setup: hal.uart.Setup = .{ .cpu_frequency = clock_frequencies.cpu }; +/// SPI on PB5 (SCK), PB3 (MOSI) and PB4 (MISO), wired to the radio. PB2 (D10) +/// is set as an output to keep the SPI in host mode. +pub const spi_setup: hal.spi.Setup = .{ .cpu_frequency = clock_frequencies.cpu }; + pub const pin_map = .{ // Port D .D0 = "PD0", @@ -54,8 +58,8 @@ pub const pin_map = .{ .NRF_CE = "PD7", .NRF_CS = "PB0", .SCK = "PB5", - .HODI = "PB3", - .HIDO = "PB4", + .MOSI = "PB3", + .MISO = "PB4", // I2C (TWI) .SDA = "PC4", diff --git a/port/microchip/atmega/src/hals/ATmega328P.zig b/port/microchip/atmega/src/hals/ATmega328P.zig index 5606dda7c..4e2163f37 100644 --- a/port/microchip/atmega/src/hals/ATmega328P.zig +++ b/port/microchip/atmega/src/hals/ATmega328P.zig @@ -1,6 +1,7 @@ const microzig = @import("microzig"); const cpu = microzig.cpu; +pub const spi = @import("atmega328p/spi.zig"); pub const uart = @import("atmega328p/uart.zig"); pub const gpio = struct { diff --git a/port/microchip/atmega/src/hals/atmega328p/spi.zig b/port/microchip/atmega/src/hals/atmega328p/spi.zig new file mode 100644 index 000000000..48e4cc6c9 --- /dev/null +++ b/port/microchip/atmega/src/hals/atmega328p/spi.zig @@ -0,0 +1,175 @@ +//! SPI host for the ATmega328P (polling). +//! +//! SCK is PB5, MOSI is PB3 and MISO is PB4. PB2 is the peripheral's own SS +//! pin: it is set as an output, because an SS input pulled low would switch +//! the SPI into device mode. Chip select is not handled here; the caller +//! drives its own CS pin. + +const std = @import("std"); +const microzig = @import("microzig"); +const gpio = microzig.hal.gpio; + +// The generated `peripherals.SPI` is a pointer to address 0 (not `allowzero`) +// with absolute register offsets, so it can't be used. Each register gets its +// own pointer instead, at its offset (which is its data-space address). +const SPI_Regs = microzig.chip.types.peripherals.SPI; +const SPCR: *volatile @FieldType(SPI_Regs, "SPCR") = @ptrFromInt(@offsetOf(SPI_Regs, "SPCR")); +const SPSR: *volatile @FieldType(SPI_Regs, "SPSR") = @ptrFromInt(@offsetOf(SPI_Regs, "SPSR")); +const SPDR: *volatile u8 = @ptrFromInt(@offsetOf(SPI_Regs, "SPDR")); + +const ss = gpio.pin(.b, 2); +const mosi = gpio.pin(.b, 3); +const sck = gpio.pin(.b, 5); + +pub const Polarity = enum(u1) { + idle_low = 0, // CPOL = 0 + idle_high = 1, // CPOL = 1 +}; + +pub const Phase = enum(u1) { + first_edge = 0, // CPHA = 0 + second_edge = 1, // CPHA = 1 +}; + +pub const BitOrder = enum(u1) { + msb_first = 0, + lsb_first = 1, +}; + +/// Board-level SPI setup. Boards export a `spi_setup` const of this type; +/// the application passes its own settings (clock, mode, etc.) to `apply`. +pub const Setup = struct { + /// The ATmega328P has a single SPI peripheral. + instance: SPI = .spi0, + /// CPU clock in Hz. + cpu_frequency: u32, + + /// Configures the pins and enables the SPI in host mode. + pub fn apply(comptime setup: Setup, comptime config: Config) void { + setup.instance.apply(setup.cpu_frequency, config); + } +}; + +/// The transfers poll the peripheral without a timeout: the SPI must be set +/// up with `apply`, and nothing else may use it at the same time. +pub const SPI = enum { + spi0, + + /// Configures the pins and enables the SPI in host mode. + pub fn apply(comptime _: SPI, comptime cpu_frequency: u32, comptime config: Config) void { + const clock = comptime compute_clock(cpu_frequency, config.baud_rate); + + // MISO (PB4) needs no setup: host mode forces it to be an input. + ss.set_direction(.output); // see the file comment + mosi.set_direction(.output); + sck.set_direction(.output); + + SPSR.write_raw(@intFromBool(clock.double_speed)); // SPI2X + SPCR.write(.{ + .SPR = @fromBackingInt(@intCast(clock.spr)), + .CPHA = @backingInt(config.phase), + .CPOL = @backingInt(config.polarity), + .MSTR = 1, + .DORD = @backingInt(config.bit_order), + .SPE = 1, + .SPIE = 0, + }); + } + /// Sends `data`, discarding what is received. + pub fn write_blocking(_: SPI, data: []const u8) void { + for (data) |byte| _ = transfer_byte(byte); + } + /// Sends each chunk in order, as one transfer. + pub fn writev_blocking(spi: SPI, chunks: []const []const u8) void { + for (chunks) |chunk| spi.write_blocking(chunk); + } + /// Fills `data`, sending 0xFF. + pub fn read_blocking(_: SPI, data: []u8) void { + for (data) |*byte| byte.* = transfer_byte(0xFF); + } + /// Fills each chunk in order, as one transfer. + pub fn readv_blocking(spi: SPI, chunks: []const []u8) void { + for (chunks) |chunk| spi.read_blocking(chunk); + } + /// Sends `tx_data` and receives into `rx_data` at the same time. Both must + /// have the same length. + pub fn transceive_blocking(_: SPI, tx_data: []const u8, rx_data: []u8) void { + for (tx_data, rx_data) |tx_byte, *rx_byte| rx_byte.* = transfer_byte(tx_byte); + } +}; + +pub const instance = struct { + pub const SPI0: SPI = .spi0; +}; + +pub const Config = struct { + /// Highest allowed SCK frequency in Hz. The actual frequency is the + /// fastest CPU clock divider (2 to 128) that doesn't exceed it. + baud_rate: u32 = 1_000_000, + polarity: Polarity = .idle_low, + phase: Phase = .first_edge, + bit_order: BitOrder = .msb_first, +}; + +const Clock = struct { + spr: u2, + double_speed: bool, +}; + +/// Returns the fastest divider that doesn't exceed `baud_rate`, or null if +/// even the slowest one does. +fn find_clock(cpu_frequency: comptime_int, baud_rate: comptime_int) ?Clock { + // { divider, SPR1:0, SPI2X }, from fastest to slowest (datasheet table 18-5). + + const dividers = .{ + .{ 2, 0b00, true }, + .{ 4, 0b00, false }, + .{ 8, 0b01, true }, + .{ 16, 0b01, false }, + .{ 32, 0b10, true }, + .{ 64, 0b10, false }, + .{ 128, 0b11, false }, + }; + inline for (dividers) |d| { + if (cpu_frequency <= baud_rate * d[0]) return .{ .spr = d[1], .double_speed = d[2] }; + } + + return null; +} + +fn compute_clock(cpu_frequency: comptime_int, baud_rate: comptime_int) Clock { + const clock = comptime find_clock(cpu_frequency, baud_rate); + if (clock == null) + @compileError(std.fmt.comptimePrint( + "SPI clock {d} Hz is below the slowest one available ({d} Hz)", + .{ baud_rate, cpu_frequency / 128 }, + )); + + return clock.?; +} + +test find_clock { + // The slowest SCK at 16 MHz is 125 kHz. + // TODO: test the compile error itself (https://github.com/ziglang/zig/issues/513). + try std.testing.expectEqual(null, find_clock(16_000_000, 100_000)); +} + +test compute_clock { + try std.testing.expectEqual(Clock{ .spr = 0b00, .double_speed = true }, compute_clock(16_000_000, 8_000_000)); + try std.testing.expectEqual(Clock{ .spr = 0b01, .double_speed = false }, compute_clock(16_000_000, 1_000_000)); + try std.testing.expectEqual(Clock{ .spr = 0b01, .double_speed = false }, compute_clock(16_000_000, 1_500_000)); + try std.testing.expectEqual(Clock{ .spr = 0b11, .double_speed = false }, compute_clock(16_000_000, 125_000)); + try std.testing.expectEqual(Clock{ .spr = 0b01, .double_speed = true }, compute_clock(16_000_000, 2_000_000)); + try std.testing.expectEqual(Clock{ .spr = 0b10, .double_speed = true }, compute_clock(16_000_000, 500_000)); + try std.testing.expectEqual(Clock{ .spr = 0b10, .double_speed = false }, compute_clock(16_000_000, 250_000)); + + // 16_000_001 / 2 is just above 8 MHz, so it needs the next divider. + try std.testing.expectEqual(Clock{ .spr = 0b00, .double_speed = false }, compute_clock(16_000_001, 8_000_000)); +} + +/// Sends `byte` and returns the byte received at the same time. +fn transfer_byte(byte: u8) u8 { + SPDR.* = byte; + while (SPSR.read().SPIF == 0) {} + return SPDR.*; +}