Repository navigation
atmega: add SPI host HAL for the ATmega328P #1076
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
93b7f92
0a345bd
59cf88a
8c39e7f
5b9f73f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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| { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason you didn't use the root of the hal for the tests? Then we wouldn't have to compile multiple. |
||
| 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); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 }; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Grazfather its goofy that the cpu frequency for the spi is being set in the board file.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The CPU frequency is defined by the board. The 3v3 sets 8MHz and the 5v sets 16MHz on the pro mini, for example. It's just passed to the spi setup so that the correct clock divider can be configured. The baud_rate is actually provided by the example. |
||
|
|
||
| pub const pin_map = .{ | ||
| // Port A | ||
| .D0 = "PD0", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 = .{ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the pin map even used for anything? If not I'd like to delete it.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it is. This is basically the same in all atmega boards. I agree we should do something or trash it, but let's clean it up in a followup, since it touches a bunch of boards
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| // 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", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| //! SPI host for the ATmega328P (polling). | ||
|
Grazfather marked this conversation as resolved.
|
||
| //! | ||
| //! 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.*; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use stdlib formatting, don't write your own.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doesn't compile when I tried.