From e1c1466d9ee0cc9e4e1303002fd00e88b0932434 Mon Sep 17 00:00:00 2001 From: Marnix Klooster Date: Wed, 1 Feb 2023 18:54:37 +0100 Subject: [PATCH] Initial partial SPI support (#97) * build.zig: Trivial rename around UART test * mmio: Add writeRaw() to set a full register * UART: Add TODO for auto baud rate detection * STM32F30x Initial USART1 output/transmit support All code assumes default chip clock configuration. Code assumes STM32F303xB / STM32F3030xC. Code supports only 8 data bits, 1 stop bit. * stm32f3discovery @panic() to UART1 This is done by implementing `debugWrite()` for the board, which only initializes UART1 if that was not yet done, and flushes afterwards to make sure the host receives all. * stm32f303: Support UART1 reader This is done by implementing `rx()` and `canRead()`. * stm32f303 UART1 correctly support 7 and 8 bits This includes correctly masking the parity bit on reads. * stm32f3 UART1 support 0.5/1.5/2 stop bits * stm32f303 UART1 simplify parity code * stm32f303 I2C rough initial code Allows only writing and reading single bytes. * stm32f3 i2c: enable debug 'logging' * Add a few comments * I2C API changes, STM32F303 I2C multi-byte transfers Now using controller/device terminology, instead of master/slave. Now using 'transfer objects' to make STOPs and re-STARTs explicit, and allow using Writer and Reader APIs. Added 'register' abstraction. STM32F303 I2C now supports this new API, and multi-byte transfers. Now waiting for I2C_ISR.BUSY == 0, after setting I2C_CR2.STOP == 1. Without this, the sequence write-stop-write caused an additional STOP to be sent immediately the START and address of the second write. * Make work with regz-generated registers.zig change * Updated to match regz-generated update * After #23 repair Reset on stm32, lpc1768 * Clean-up I2C `readRegisters()`. * Refactor to separate read/write states * On STM32F303, make second read call fail Also doc comments to clarify the new API. * STM32 I2C: Properly support multiple write calls * I2C STM32: Fix release mode compile error ...on top of an earlier commit on this branch. * I2C Add 'write register' shorthand functions * Make sure vector_table is correctly exported It needs to be a non-`comptime` `var` for `@export` to work properly. The only 'documentation' for this behavior currently seems GitHub comment https://github.com/ziglang/zig/issues/5157#issuecomment-618933196 . This issue was introduced in 1c17304 for PR #27, which broke at least ARM-based STM32F303. * fix missing vector table on ARM * Revert "Merge branch 'fix-vector_table-export' into marnix-master" This reverts commit 8ea0a74e1031cd0b88abe0283f179f0cf20f450c, reversing changes made to 355a3618080d28c5da6e044773e6449989355fe5. * Temp commit for SPI * Check new I2C target_speed config setting * Corrected incorrect doc comment * Initial SPI transfer support for STM32F303 * SPI device CS pin is now used * Revert accidentally committed debug flag. * SPI: Add shorthands for 'register-based' devices. * Additional fix to remove PE3 pin dependency * SPI: Renames, comments, extracted device-specific code Specifically, top-level `Spi` is now `SpiBus`; and the internal API has an additional `switchToDevice()`, and receives `DeviceConfig` in more places. * SPI device: Add `transceive()` method. --------- Co-authored-by: Matt Knight --- src/core/microzig.zig | 3 + src/core/spi.zig | 146 ++++++++++++++++++++++ src/modules/chips/stm32f303/stm32f303.zig | 129 ++++++++++++++++++- 3 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 src/core/spi.zig diff --git a/src/core/microzig.zig b/src/core/microzig.zig index fe7b648..5440e24 100644 --- a/src/core/microzig.zig +++ b/src/core/microzig.zig @@ -41,6 +41,9 @@ pub const Pin = pin.Pin; pub const uart = @import("uart.zig"); pub const Uart = uart.Uart; +pub const spi = @import("spi.zig"); +pub const SpiBus = spi.SpiBus; + pub const i2c = @import("i2c.zig"); pub const I2CController = i2c.I2CController; diff --git a/src/core/spi.zig b/src/core/spi.zig new file mode 100644 index 0000000..af8e3c5 --- /dev/null +++ b/src/core/spi.zig @@ -0,0 +1,146 @@ +const std = @import("std"); +const micro = @import("microzig.zig"); +const chip = @import("chip"); + +/// The SPI bus with the given environment-specific number. +/// Only 'master' mode is supported currently. +pub fn SpiBus(comptime index: usize) type { + const SystemSpi = chip.SpiBus(index); + + return struct { + /// A SPI 'slave' device, selected via the given CS pin. + /// (Default is CS=low to select.) + pub fn SpiDevice(comptime cs_pin: type, config: DeviceConfig) type { + return struct { + const SelfSpiDevice = @This(); + + internal: SystemSpi, + + /// A 'transfer' is defined as a sequence of reads/writes that require + /// the SPI device to be continuously enabled via its 'chip select' (CS) line. + const Transfer = struct { + const SelfTransfer = @This(); + + device: SelfSpiDevice, + + fn transceiveByte(self: *SelfTransfer, write_byte: u8, read_pointer: *u8) !void { + try self.device.internal.transceiveByte(write_byte, read_pointer); + } + + pub const Writer = std.io.Writer(*SelfTransfer, WriteError, writeSome); + + /// Return a standard Writer (which ignores the bytes read). + pub fn writer(self: *SelfTransfer) Writer { + return Writer{ .context = self }; + } + + fn writeSome(self: *SelfTransfer, buffer: []const u8) WriteError!usize { + try self.device.internal.writeAll(buffer); + return buffer.len; + } + + pub const Reader = std.io.Reader(*SelfTransfer, ReadError, readSome); + + /// Return a standard Reader (which writes arbitrary bytes). + pub fn reader(self: *SelfTransfer) Reader { + return Reader{ .context = self }; + } + + fn readSome(self: *SelfTransfer, buffer: []u8) ReadError!usize { + try self.device.internal.readInto(buffer); + return buffer.len; + } + + /// end the current transfer, releasing via the CS pin + pub fn end(self: *SelfTransfer) void { + self.device.internal.endTransfer(cs_pin, config); + } + }; + + /// start a new transfer, selecting using the CS pin + pub fn beginTransfer(self: SelfSpiDevice) !Transfer { + self.internal.switchToDevice(cs_pin, config); + self.internal.beginTransfer(cs_pin, config); + return Transfer{ .device = self }; + } + + pub fn transceive(self: SelfSpiDevice, write_buffer: []const u8, read_buffer: []u8) !void { + std.debug.assert(write_buffer.len == read_buffer.len); + var transfer = try self.beginTransfer(); + defer transfer.end(); + for (write_buffer) |_, i| { + try transfer.transceiveByte(write_buffer[i], &read_buffer[i]); + } + } + + /// Shorthand for 'register-based' devices + pub fn writeRegister(self: SelfSpiDevice, register_address: u8, byte: u8) ReadError!void { + try self.writeRegisters(register_address, &.{byte}); + } + + /// Shorthand for 'register-based' devices + pub fn writeRegisters(self: SelfSpiDevice, register_address: u8, buffer: []u8) ReadError!void { + var transfer = try self.beginTransfer(); + defer transfer.end(); + // write auto-increment, starting at given register + try transfer.writer().writeByte(0b01_000000 | register_address); + try transfer.writer().writeAll(buffer); + } + + /// Shorthand for 'register-based' devices + pub fn readRegister(self: SelfSpiDevice, register_address: u8) ReadError!u8 { + var buffer: [1]u8 = undefined; + try self.readRegisters(register_address, &buffer); + return buffer[0]; + } + + /// Shorthand for 'register-based' devices + pub fn readRegisters(self: SelfSpiDevice, register_address: u8, buffer: []u8) ReadError!void { + var transfer = try self.beginTransfer(); + defer transfer.end(); + // read auto-increment, starting at given register + try transfer.writer().writeByte(0b11_000000 | register_address); + try transfer.reader().readNoEof(buffer); + } + }; + } + + const SelfSpiBus = @This(); + + internal: SystemSpi, + + /// Initialize this SPI bus and return a handle to it. + pub fn init(config: BusConfig) InitError!SelfSpiBus { + micro.clock.ensure(); // TODO: Wat? + return SelfSpiBus{ + .internal = try SystemSpi.init(config), + }; + } + + /// Create (a descriptor for) a device on this SPI bus. + pub fn device(self: SelfSpiBus, comptime cs_pin: type, config: DeviceConfig) SpiDevice(cs_pin, config) { + return SpiDevice(cs_pin, config){ .internal = self.internal }; + } + }; +} + +/// A SPI bus configuration. +/// (There are no bus configuration options yet.) +pub const BusConfig = struct { + // Later: add common options +}; + +/// A SPI device configuration (excluding the CS pin). +/// (There are no device configuration options yet.) +pub const DeviceConfig = struct { + // TODO: add common options, like clock polarity and phase, and CS polarity +}; + +pub const InitError = error{ +// TODO: add common options +}; + +pub const WriteError = error{}; +pub const ReadError = error{ + EndOfStream, +}; diff --git a/src/modules/chips/stm32f303/stm32f303.zig b/src/modules/chips/stm32f303/stm32f303.zig index bb92d41..a4cfb53 100644 --- a/src/modules/chips/stm32f303/stm32f303.zig +++ b/src/modules/chips/stm32f303/stm32f303.zig @@ -281,7 +281,6 @@ pub fn I2CController(comptime index: usize, comptime pins: micro.i2c.Pins) type regs.RCC.APB1ENR.modify(.{ .I2C1EN = 1 }); regs.RCC.AHBENR.modify(.{ .IOPBEN = 1 }); debugPrint("I2C1 configuration step 1 complete\r\n", .{}); - // 2. Configure the I2C PINs for ALternate Functions // a) Select Alternate Function in MODER Register regs.GPIOB.MODER.modify(.{ .MODER6 = 0b10, .MODER7 = 0b10 }); @@ -469,3 +468,131 @@ pub fn I2CController(comptime index: usize, comptime pins: micro.i2c.Pins) type }; }; } + +/// An STM32F303 SPI bus +pub fn SpiBus(comptime index: usize) type { + if (!(index == 1)) @compileError("TODO: only SPI1 is currently supported"); + + return struct { + const Self = @This(); + + /// Initialize and enable the bus. + pub fn init(config: micro.spi.BusConfig) !Self { + _ = config; // unused for now + + // CONFIGURE SPI1 + // connected to APB2, MCU pins PA5 + PA7 + PA6 = SPC + SDI + SDO, + // if GPIO port A is configured for alternate function 5 for these PA pins. + + // Enable the GPIO CLOCK + regs.RCC.AHBENR.modify(.{ .IOPAEN = 1 }); + + // Configure the I2C PINs for ALternate Functions + // - Select Alternate Function in MODER Register + regs.GPIOA.MODER.modify(.{ .MODER5 = 0b10, .MODER6 = 0b10, .MODER7 = 0b10 }); + // - Select High SPEED for the PINs + regs.GPIOA.OSPEEDR.modify(.{ .OSPEEDR5 = 0b11, .OSPEEDR6 = 0b11, .OSPEEDR7 = 0b11 }); + // - Configure the Alternate Function in AFR Register + regs.GPIOA.AFRL.modify(.{ .AFRL5 = 5, .AFRL6 = 5, .AFRL7 = 5 }); + + // Enable the SPI1 CLOCK + regs.RCC.APB2ENR.modify(.{ .SPI1EN = 1 }); + + regs.SPI1.CR1.modify(.{ + .MSTR = 1, + .SSM = 1, + .SSI = 1, + .RXONLY = 0, + .SPE = 1, + }); + // the following configuration is assumed in `transceiveByte()` + regs.SPI1.CR2.raw = 0; + regs.SPI1.CR2.modify(.{ + .DS = 0b0111, // 8-bit data frames, seems default via '0b0000 is interpreted as 0b0111' + .FRXTH = 1, // RXNE event after 1 byte received + }); + + return Self{}; + } + + /// Switch this SPI bus to the given device. + pub fn switchToDevice(_: Self, comptime cs_pin: type, config: micro.spi.DeviceConfig) void { + _ = config; // for future use + + regs.SPI1.CR1.modify(.{ + .CPOL = 1, // TODO: make configurable + .CPHA = 1, // TODO: make configurable + .BR = 0b111, // 1/256 the of PCLK TODO: make configurable + .LSBFIRST = 0, // MSB first TODO: make configurable + }); + gpio.setOutput(cs_pin); + } + + /// Begin a transfer to the given device. (Assumes `switchToDevice()` was called.) + pub fn beginTransfer(_: Self, comptime cs_pin: type, config: micro.spi.DeviceConfig) void { + _ = config; // for future use + gpio.write(cs_pin, .low); // select the given device, TODO: support inverse CS devices + debugPrint("enabled SPI1\r\n", .{}); + } + + /// The basic operation in the current simplistic implementation: + /// send+receive a single byte. + /// Writing `null` writes an arbitrary byte (`undefined`), and + /// reading into `null` ignores the value received. + pub fn transceiveByte(_: Self, optional_write_byte: ?u8, optional_read_pointer: ?*u8) !void { + + // SPIx_DR's least significant byte is `@bitCast([dr_byte_size]u8, ...)[0]` + const dr_byte_size = @sizeOf(@TypeOf(regs.SPI1.DR.raw)); + + // wait unril ready for write + while (regs.SPI1.SR.read().TXE == 0) { + debugPrint("SPI1 TXE == 0\r\n", .{}); + } + debugPrint("SPI1 TXE == 1\r\n", .{}); + + // write + const write_byte = if (optional_write_byte) |b| b else undefined; // dummy value + @bitCast([dr_byte_size]u8, regs.SPI1.DR.*)[0] = write_byte; + debugPrint("Sent: {X:2}.\r\n", .{write_byte}); + + // wait until read processed + while (regs.SPI1.SR.read().RXNE == 0) { + debugPrint("SPI1 RXNE == 0\r\n", .{}); + } + debugPrint("SPI1 RXNE == 1\r\n", .{}); + + // read + var data_read = regs.SPI1.DR.raw; + _ = regs.SPI1.SR.read(); // clear overrun flag + const dr_lsb = @bitCast([dr_byte_size]u8, data_read)[0]; + debugPrint("Received: {X:2} (DR = {X:8}).\r\n", .{ dr_lsb, data_read }); + if (optional_read_pointer) |read_pointer| read_pointer.* = dr_lsb; + } + + /// Write all given bytes on the bus, not reading anything back. + pub fn writeAll(self: Self, bytes: []const u8) !void { + for (bytes) |b| { + try self.transceiveByte(b, null); + } + } + + /// Read bytes to fill the given buffer exactly, writing arbitrary bytes (`undefined`). + pub fn readInto(self: Self, buffer: []u8) !void { + for (buffer) |_, i| { + try self.transceiveByte(null, &buffer[i]); + } + } + + pub fn endTransfer(_: Self, comptime cs_pin: type, config: micro.spi.DeviceConfig) void { + _ = config; // for future use + // no delay should be needed here, since we know SPIx_SR's TXE is 1 + debugPrint("(disabling SPI1)\r\n", .{}); + gpio.write(cs_pin, .high); // deselect the given device, TODO: support inverse CS devices + // HACK: wait long enough to make any device end an ongoing transfer + var i: u8 = 255; // with the default clock, this seems to delay ~185 microseconds + while (i > 0) : (i -= 1) { + asm volatile ("nop"); + } + } + }; +}