diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3cc2a8f78..fd8fae52a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -170,7 +170,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - port_dir: [gigadevice/gd32, raspberrypi/rp2xxx, stmicro/stm32] + port_dir: [gigadevice/gd32, raspberrypi/rp2xxx, stmicro/stm32, wch/ch32v] steps: - name: Checkout uses: actions/checkout@v4 diff --git a/examples/wch/ch32v/build.zig b/examples/wch/ch32v/build.zig index f342bd5a4..43d04c258 100644 --- a/examples/wch/ch32v/build.zig +++ b/examples/wch/ch32v/build.zig @@ -35,6 +35,7 @@ pub fn build(b: *std.Build) void { .{ .target = mb.ports.ch32v.boards.ch32v203.lana_tny, .name = "lana_tny_ws2812", .file = "src/ws2812.zig" }, .{ .target = mb.ports.ch32v.boards.ch32v203.lana_tny, .name = "lana_tny_uart_log", .file = "src/uart_log.zig" }, .{ .target = mb.ports.ch32v.boards.ch32v203.lana_tny, .name = "lana_tny_i2c_bus_scan", .file = "src/i2c_bus_scan.zig" }, + .{ .target = mb.ports.ch32v.boards.ch32v203.lana_tny, .name = "lana_tny_i2c_position_sensor", .file = "src/i2c_position_sensor.zig" }, // CH32V30x .{ .target = mb.ports.ch32v.chips.ch32v303xb, .name = "empty_ch32v303", .file = "src/empty.zig" }, diff --git a/examples/wch/ch32v/src/i2c_position_sensor.zig b/examples/wch/ch32v/src/i2c_position_sensor.zig new file mode 100644 index 000000000..0c056bb45 --- /dev/null +++ b/examples/wch/ch32v/src/i2c_position_sensor.zig @@ -0,0 +1,81 @@ +const std = @import("std"); +const microzig = @import("microzig"); +const mdf = microzig.drivers; +const hal = microzig.hal; + +const gpio = hal.gpio; +const i2c = hal.i2c; + +const I2C_Device = hal.drivers.I2C_Device; +const AS5600 = microzig.drivers.sensor.AS5600; + +const usart = hal.usart.instance.USART2; +const usart_tx_pin = gpio.Pin.init(0, 2); // PA2 + +pub const microzig_options = microzig.Options{ + .log_level = .info, + .logFn = hal.usart.log, +}; + +pub fn main() !void { + // Board brings up clocks and time + microzig.board.init(); + + // Configure USART2 TX pin (PA2) for alternate function (disable GPIO) + usart_tx_pin.configure_alternate_function(.push_pull, .max_50MHz); + + // Initialize USART2 at 115200 baud (uses default pins PA2/PA3) + usart.apply(.{ + .baud_rate = 115200, + .remap = .default, + }); + + hal.usart.init_logger(usart); + + // I2C1 is on PB6 (SCL) and PB7 (SDA) + const scl_pin = hal.gpio.Pin.init(1, 6); // GPIOB pin 6 + const sda_pin = hal.gpio.Pin.init(1, 7); // GPIOB pin 7 + + // Configure I2C pins for alternate function (open-drain required for I2C) + scl_pin.configure_alternate_function(.open_drain, .max_50MHz); + sda_pin.configure_alternate_function(.open_drain, .max_50MHz); + + const instance = i2c.instance.I2C1; + const i2c_config = i2c.Config{ + .baud_rate = 100_000, // 100 kHz + .dma = .{ + .tx_channel = .Ch6, // I2C1 TX must use Ch6 + .rx_channel = .Ch7, // I2C1 RX must use Ch7 + .priority = .High, + .threshold = 4, // Threshold for DMA transfers + }, + }; + instance.apply(i2c_config); + + // Get the specialized I2C_Device type for this config + const I2C_DeviceType = hal.drivers.I2C_Device(i2c_config); + + // Create i2c device + var i2c_device = I2C_DeviceType.init(instance, null); + // Pass device to driver to create sensor instance + std.log.info("Creating AS5600 driver instance", .{}); + var dev = AS5600.init(i2c_device.i2c_device()); + + std.log.info("Starting position sensor reads...", .{}); + + while (true) { + const status = try dev.read_status(); + if (status.MD != 0 and status.MH == 0 and status.ML == 0) { + const raw_angle = try dev.read_raw_angle(); + std.log.info("Raw Angle: {d:0.2}°", .{raw_angle}); + const angle = try dev.read_angle(); + std.log.info("Angle: {d:0.2}°", .{angle}); + const magnitude = try dev.read_magnitude(); + std.log.info("Magnitude: {any}", .{magnitude}); + } else { + std.log.warn("Magnet status - MD:{} MH:{} ML:{}", .{ status.MD, status.MH, status.ML }); + } + + hal.time.sleep_ms(250); + } +} diff --git a/port/wch/ch32v/build.zig b/port/wch/ch32v/build.zig index c4a12a189..64e36e81f 100644 --- a/port/wch/ch32v/build.zig +++ b/port/wch/ch32v/build.zig @@ -243,5 +243,15 @@ pub fn init(dep: *std.Build.Dependency) Self { } pub fn build(b: *std.Build) void { - _ = b.step("test", "Run platform agnostic unit tests"); + const test_step = b.step("test", "Run platform agnostic unit tests"); + + // Test DMA HAL + const dma_tests = b.addTest(.{ + .root_module = b.createModule(.{ + .root_source_file = b.path("src/hals/dma.zig"), + .target = b.graph.host, + }), + }); + const run_dma_tests = b.addRunArtifact(dma_tests); + test_step.dependOn(&run_dma_tests.step); } diff --git a/port/wch/ch32v/src/hals/dma.zig b/port/wch/ch32v/src/hals/dma.zig index f6f64963e..0d0164488 100644 --- a/port/wch/ch32v/src/hals/dma.zig +++ b/port/wch/ch32v/src/hals/dma.zig @@ -33,8 +33,10 @@ pub const PeripheralTarget = struct { addr: u32, }; +pub const Priority = enum(u2) { Low = 0, Medium = 1, High = 2, VeryHigh = 3 }; + pub const TransferConfig = struct { - priority: enum(u2) { Low = 0, Medium = 1, High = 2, VeryHigh = 3 } = .Medium, + priority: Priority = .Medium, // AKA cycle mode. NOTE: When set, the transfer will never complete and will need to be stopped // manually. // Also note that this mode is not supported in Mem2Mem mode @@ -329,3 +331,175 @@ pub const Channel = enum(u3) { return regs.CNTR.read().NDT; } }; + +/// DMA peripheral mapping for CH32V203 +/// Based on CH32V20x Reference Manual DMA Request Mapping Table +pub const Peripheral = enum { + // I2C + I2C1_TX, + I2C1_RX, + I2C2_TX, + I2C2_RX, + + // USART (uncomment when UART HAL gets DMA support) + // USART1_TX, + // USART1_RX, + // USART2_TX, + // USART2_RX, + // USART3_TX, + // USART3_RX, + // USART4_RX, + // USART4_TX, + // USART5_RX, + // USART5_TX, + // USART6_TX, + // USART6_RX, + + // SPI (uncomment when SPI HAL gets DMA support) + // SPI1_RX, + // SPI1_TX, + // SPI2_TX, + // SPI3_RX, + // SPI3_TX, + + // ADC (uncomment when ADC HAL gets DMA support) + // ADC1, + + // Timers (uncomment when Timer HAL gets DMA support) + // TIM1_CH1, TIM1_CH2, TIM1_CH3, TIM1_CH4, TIM1_UP, TIM1_TRIG, TIM1_COM, + // TIM2_CH1, TIM2_CH2, TIM2_CH3, TIM2_CH4, TIM2_UP, + // TIM3_CH1, TIM3_CH3, TIM3_CH4, TIM3_UP, TIM3_TRIG, + // TIM4_CH1, TIM4_CH2, TIM4_CH3, TIM4_UP, + // TIM5_CH1, TIM5_CH2, TIM5_CH3, TIM5_CH4, TIM5_UP, TIM5_TRIG, + // TIM6_UP, TIM7_UP, + // TIM8_CH1, TIM8_CH2, TIM8_CH3, TIM8_CH4, TIM8_UP, TIM8_TRIG, TIM8_COM, + // TIM9_UP, TIM9_CH1, + // TIM10_CH4, TIM10_TRIG, TIM10_COM, + + // DAC (uncomment when DAC HAL gets DMA support) + // DAC1, DAC2, + + // SDIO (uncomment when SDIO HAL gets DMA support) + // SDIO, + + /// Get valid DMA channels for this peripheral (compile-time) + /// Returns a slice of valid channels from TRM mapping table + pub fn get_valid_channels(comptime self: Peripheral) []const Channel { + return comptime switch (self) { + // I2C mappings from CH32V203 TRM Table 11-1 + .I2C1_TX => &[_]Channel{.Ch6}, + .I2C1_RX => &[_]Channel{.Ch7}, + .I2C2_TX => &[_]Channel{.Ch4}, + .I2C2_RX => &[_]Channel{.Ch5}, + + // NOTE: Add mappings here when adding support for new peripherals and uncommenting + // enums above + // .USART1_TX => &[_]Channel{.Ch4}, + // .USART1_RX => &[_]Channel{.Ch5}, + // .USART2_TX => &[_]Channel{.Ch7}, + // .USART2_RX => &[_]Channel{.Ch6}, + // .SPI1_TX => &[_]Channel{.Ch3}, + // .SPI1_RX => &[_]Channel{.Ch2}, + // .ADC1 => &[_]Channel{.Ch1}, + // etc... + }; + } + + /// Validate that a channel is valid for this peripheral (testable) + /// Returns error.InvalidChannel if the combination is invalid. + pub fn validate_channel( + comptime self: Peripheral, + comptime channel: Channel, + ) error{InvalidChannel}!void { + const is_valid = comptime blk: { + const valid_channels = self.get_valid_channels(); + for (valid_channels) |valid_ch| { + if (valid_ch == channel) break :blk true; + } + break :blk false; + }; + + if (!is_valid) return error.InvalidChannel; + } + + /// Validate that a channel is valid for this peripheral (compile-time) + /// Generates helpful compile error if the combination is invalid. + pub fn ensure_compatible_with(comptime self: Peripheral, comptime channel: Channel) void { + self.validate_channel(channel) catch { + // Build helpful error message showing valid options + const valid_channels = comptime self.get_valid_channels(); + + // Build list of valid channel names + comptime var channel_list: []const u8 = ""; + inline for (valid_channels, 0..) |valid_ch, i| { + if (i > 0) channel_list = channel_list ++ ", "; + channel_list = channel_list ++ @tagName(valid_ch); + } + + const msg = comptime std.fmt.comptimePrint( + "DMA Channel {s} is not valid for {s}. Valid channels: [{s}]", + .{ @tagName(channel), @tagName(self), channel_list }, + ); + + @compileError(msg); + }; + } +}; + +// ============================================================================ +// Tests +// ============================================================================ + +test "Peripheral - valid I2C channels" { + // I2C1 valid channels + try Peripheral.I2C1_TX.validate_channel(.Ch6); + try Peripheral.I2C1_RX.validate_channel(.Ch7); + + // I2C2 valid channels + try Peripheral.I2C2_TX.validate_channel(.Ch4); + try Peripheral.I2C2_RX.validate_channel(.Ch5); +} + +test "Peripheral - invalid I2C channels" { + // I2C1 TX invalid channels + try std.testing.expectError(error.InvalidChannel, Peripheral.I2C1_TX.validate_channel(.Ch1)); + try std.testing.expectError(error.InvalidChannel, Peripheral.I2C1_TX.validate_channel(.Ch2)); + try std.testing.expectError(error.InvalidChannel, Peripheral.I2C1_TX.validate_channel(.Ch3)); + try std.testing.expectError(error.InvalidChannel, Peripheral.I2C1_TX.validate_channel(.Ch4)); + try std.testing.expectError(error.InvalidChannel, Peripheral.I2C1_TX.validate_channel(.Ch5)); + try std.testing.expectError(error.InvalidChannel, Peripheral.I2C1_TX.validate_channel(.Ch7)); + + // I2C1 RX invalid channels + try std.testing.expectError(error.InvalidChannel, Peripheral.I2C1_RX.validate_channel(.Ch1)); + try std.testing.expectError(error.InvalidChannel, Peripheral.I2C1_RX.validate_channel(.Ch6)); + + // I2C2 TX invalid channels + try std.testing.expectError(error.InvalidChannel, Peripheral.I2C2_TX.validate_channel(.Ch1)); + try std.testing.expectError(error.InvalidChannel, Peripheral.I2C2_TX.validate_channel(.Ch5)); + + // I2C2 RX invalid channels + try std.testing.expectError(error.InvalidChannel, Peripheral.I2C2_RX.validate_channel(.Ch1)); + try std.testing.expectError(error.InvalidChannel, Peripheral.I2C2_RX.validate_channel(.Ch4)); +} + +test "Peripheral - get_valid_channels returns correct slices" { + // I2C1 TX should return only Ch6 + const i2c1_tx_channels = Peripheral.I2C1_TX.get_valid_channels(); + try std.testing.expectEqual(@as(usize, 1), i2c1_tx_channels.len); + try std.testing.expectEqual(Channel.Ch6, i2c1_tx_channels[0]); + + // I2C1 RX should return only Ch7 + const i2c1_rx_channels = Peripheral.I2C1_RX.get_valid_channels(); + try std.testing.expectEqual(@as(usize, 1), i2c1_rx_channels.len); + try std.testing.expectEqual(Channel.Ch7, i2c1_rx_channels[0]); + + // I2C2 TX should return only Ch4 + const i2c2_tx_channels = Peripheral.I2C2_TX.get_valid_channels(); + try std.testing.expectEqual(@as(usize, 1), i2c2_tx_channels.len); + try std.testing.expectEqual(Channel.Ch4, i2c2_tx_channels[0]); + + // I2C2 RX should return only Ch5 + const i2c2_rx_channels = Peripheral.I2C2_RX.get_valid_channels(); + try std.testing.expectEqual(@as(usize, 1), i2c2_rx_channels.len); + try std.testing.expectEqual(Channel.Ch5, i2c2_rx_channels[0]); +} diff --git a/port/wch/ch32v/src/hals/drivers.zig b/port/wch/ch32v/src/hals/drivers.zig index 336de7945..b04e30514 100644 --- a/port/wch/ch32v/src/hals/drivers.zig +++ b/port/wch/ch32v/src/hals/drivers.zig @@ -20,103 +20,112 @@ const I2CAddress = drivers.I2C_Device.Address; /// /// A Implementation of the I2C_Device interface +/// Generic over I2C configuration to enable compile-time DMA optimization /// -pub const I2C_Device = struct { - /// Selects which I²C bus should be used. - bus: i2c.I2C, +pub fn I2C_Device(comptime config: i2c.Config) type { + return struct { + const Self = @This(); - /// Default timeout duration - timeout: ?mdf.time.Duration = null, + /// Selects which I²C bus should be used. + bus: i2c.I2C, - pub fn init(bus: i2c.I2C, timeout: ?mdf.time.Duration) I2C_Device { - return .{ - .bus = bus, - .timeout = timeout, - }; - } + /// Default timeout duration + timeout: ?mdf.time.Duration = null, - pub fn i2c_device(dev: *I2C_Device) drivers.I2C_Device { - return .{ - .ptr = dev, - .vtable = &i2c_vtable, - }; - } + pub fn init(bus: i2c.I2C, timeout: ?mdf.time.Duration) Self { + return .{ + .bus = bus, + .timeout = timeout, + }; + } - pub fn write(dev: I2C_Device, address: I2CAddress, buf: []const u8) I2CError!void { - return dev.bus.write_blocking(address, buf, dev.timeout) catch |err| switch (err) { - error.ArbitrationLost, error.BusError, error.Overrun => I2CError.UnknownAbort, - else => |e| e, - }; - } + pub fn i2c_device(dev: *Self) drivers.I2C_Device { + return .{ + .ptr = dev, + .vtable = &i2c_vtable, + }; + } - pub fn writev(dev: I2C_Device, address: I2CAddress, chunks: []const []const u8) I2CError!void { - return dev.bus.writev_blocking(address, chunks, dev.timeout) catch |err| switch (err) { - error.ArbitrationLost, error.BusError, error.Overrun => I2CError.UnknownAbort, - else => |e| e, - }; - } + pub fn write(dev: Self, address: I2CAddress, buf: []const u8) I2CError!void { + return dev.bus.write_auto(config, address, buf, dev.timeout) catch |err| switch (err) { + error.ArbitrationLost, error.BusError, error.Overrun => I2CError.UnknownAbort, + else => |e| e, + }; + } - pub fn read(dev: I2C_Device, address: I2CAddress, buf: []u8) I2CError!usize { - dev.bus.read_blocking(address, buf, dev.timeout) catch |err| return switch (err) { - error.ArbitrationLost, error.BusError, error.Overrun => I2CError.UnknownAbort, - else => |e| e, - }; - return buf.len; - } + pub fn writev(dev: Self, address: I2CAddress, chunks: []const []const u8) I2CError!void { + return dev.bus.writev_auto(config, address, chunks, dev.timeout) catch |err| switch (err) { + error.ArbitrationLost, error.BusError, error.Overrun => I2CError.UnknownAbort, + else => |e| e, + }; + } - pub fn readv(dev: I2C_Device, address: I2CAddress, chunks: []const []u8) I2CError!usize { - dev.bus.readv_blocking(address, chunks, dev.timeout) catch |err| return switch (err) { - error.ArbitrationLost, error.BusError, error.Overrun => I2CError.UnknownAbort, - else => |e| e, - }; - return microzig.utilities.SliceVector([]u8).init(chunks).size(); - } + pub fn read(dev: Self, address: I2CAddress, buf: []u8) I2CError!usize { + dev.bus.read_auto(config, address, buf, dev.timeout) catch |err| return switch (err) { + error.ArbitrationLost, error.BusError, error.Overrun => I2CError.UnknownAbort, + else => |e| e, + }; + return buf.len; + } - pub fn write_then_read(dev: I2C_Device, address: I2CAddress, src: []const u8, dst: []u8) I2CError!void { - dev.bus.write_then_read_blocking(address, src, dst, dev.timeout) catch |err| return switch (err) { - error.ArbitrationLost, error.BusError, error.Overrun => I2CError.UnknownAbort, - else => |e| e, - }; - } + pub fn readv(dev: Self, address: I2CAddress, chunks: []const []u8) I2CError!usize { + dev.bus.readv_auto(config, address, chunks, dev.timeout) catch |err| return switch (err) { + error.ArbitrationLost, error.BusError, error.Overrun => I2CError.UnknownAbort, + else => |e| e, + }; + return microzig.utilities.SliceVector([]u8).init(chunks).size(); + } - pub fn writev_then_readv( - dev: I2C_Device, - address: I2CAddress, - write_chunks: []const []const u8, - read_chunks: []const []u8, - ) I2CError!void { - return dev.bus.writev_then_readv_blocking(address, write_chunks, read_chunks, dev.timeout) catch |err| switch (err) { - error.ArbitrationLost, error.BusError, error.Overrun => I2CError.UnknownAbort, - else => |e| e, - }; - } + pub fn write_then_read(dev: Self, address: I2CAddress, src: []const u8, dst: []u8) I2CError!void { + const write_chunks: []const []const u8 = &.{src}; + const read_chunks: []const []u8 = &.{dst}; + dev.bus.writev_then_readv_auto(config, address, write_chunks, read_chunks, dev.timeout) catch |err| + return switch (err) { + error.ArbitrationLost, error.BusError, error.Overrun => I2CError.UnknownAbort, + else => |e| e, + }; + } - const i2c_vtable = drivers.I2C_Device.VTable{ - .writev_fn = writev_fn, - .readv_fn = readv_fn, - .writev_then_readv_fn = writev_then_readv_fn, - }; + pub fn writev_then_readv( + dev: Self, + address: I2CAddress, + write_chunks: []const []const u8, + read_chunks: []const []u8, + ) I2CError!void { + return dev.bus.writev_then_readv_auto(config, address, write_chunks, read_chunks, dev.timeout) catch |err| + switch (err) { + error.ArbitrationLost, error.BusError, error.Overrun => I2CError.UnknownAbort, + else => |e| e, + }; + } - fn writev_fn(dd: *anyopaque, address: I2CAddress, chunks: []const []const u8) I2CError!void { - const dev: *I2C_Device = @ptrCast(@alignCast(dd)); - return dev.writev(address, chunks); - } + const i2c_vtable = drivers.I2C_Device.VTable{ + .writev_fn = writev_fn, + .readv_fn = readv_fn, + .writev_then_readv_fn = writev_then_readv_fn, + }; - fn readv_fn(dd: *anyopaque, address: I2CAddress, chunks: []const []u8) I2CError!usize { - const dev: *I2C_Device = @ptrCast(@alignCast(dd)); - return dev.readv(address, chunks); - } + fn writev_fn(dd: *anyopaque, address: I2CAddress, chunks: []const []const u8) I2CError!void { + const dev: *Self = @ptrCast(@alignCast(dd)); + return dev.writev(address, chunks); + } - fn writev_then_readv_fn( - dd: *anyopaque, - address: I2CAddress, - write_chunks: []const []const u8, - read_chunks: []const []u8, - ) I2CError!void { - const dev: *I2C_Device = @ptrCast(@alignCast(dd)); - return dev.writev_then_readv(address, write_chunks, read_chunks); - } -}; + fn readv_fn(dd: *anyopaque, address: I2CAddress, chunks: []const []u8) I2CError!usize { + const dev: *Self = @ptrCast(@alignCast(dd)); + return dev.readv(address, chunks); + } + + fn writev_then_readv_fn( + dd: *anyopaque, + address: I2CAddress, + write_chunks: []const []const u8, + read_chunks: []const []u8, + ) I2CError!void { + const dev: *Self = @ptrCast(@alignCast(dd)); + return dev.writev_then_readv(address, write_chunks, read_chunks); + } + }; +} /// /// Implementation of a time device diff --git a/port/wch/ch32v/src/hals/i2c.zig b/port/wch/ch32v/src/hals/i2c.zig index d3bbb19c7..62f18a5a5 100644 --- a/port/wch/ch32v/src/hals/i2c.zig +++ b/port/wch/ch32v/src/hals/i2c.zig @@ -9,6 +9,7 @@ const microzig = @import("microzig"); const mdf = microzig.drivers; const drivers = mdf.base; const hal = microzig.hal; +const dma = hal.dma; const I2C1 = microzig.chip.peripherals.I2C1; const I2C2 = microzig.chip.peripherals.I2C2; @@ -39,10 +40,28 @@ pub const Remap = enum(u1) { }; pub const Config = struct { + /// DMA configuration for I2C transfers (optional) + /// When configured, enables DMA for transfers >= threshold bytes + /// Smaller transfers automatically use polling mode + pub const DmaConfig = struct { + tx_channel: dma.Channel, + rx_channel: dma.Channel, + priority: dma.Priority = .Medium, + /// Minimum transfer size in bytes to use DMA (smaller uses polling) + /// Default: 16 bytes (conservative heuristic, not benchmarked) + /// Lower this for testing or small-transfer optimization + /// Increase this to reduce DMA overhead for small transfers + threshold: usize = 16, + }; + repeated_start: bool = true, baud_rate: u32 = 100_000, duty_cycle: DutyCycle = .duty_2, remap: Remap = .default, + + /// Optional DMA configuration - null means polling-only mode + /// Example: .dma = .{ .tx_channel = .Ch6, .rx_channel = .Ch7 } + dma: ?DmaConfig = null, }; pub const DutyCycle = enum { @@ -85,6 +104,22 @@ pub const I2C = enum(u1) { pub fn apply(comptime i2c: I2C, comptime config: Config) void { const regs = i2c.get_regs(); + // Compile-time DMA validation + if (comptime config.dma) |dma_cfg| { + const tx_peripheral = comptime if (@intFromEnum(i2c) == 0) + dma.Peripheral.I2C1_TX + else + dma.Peripheral.I2C2_TX; + const rx_peripheral = comptime if (@intFromEnum(i2c) == 0) + dma.Peripheral.I2C1_RX + else + dma.Peripheral.I2C2_RX; + + // Ensure that these channels are compatible with these peripherals (compile error if invalid) + comptime tx_peripheral.ensure_compatible_with(dma_cfg.tx_channel); + comptime rx_peripheral.ensure_compatible_with(dma_cfg.rx_channel); + } + // Enable peripheral clock hal.clocks.enable_peripheral_clock(switch (@intFromEnum(i2c)) { 0 => .I2C1, @@ -138,7 +173,7 @@ pub const I2C = enum(u1) { ccr = result | 0x8000; // Set F/S bit for fast mode // Rise time register: ((freqrange * 300) / 1000) + 1 - const trise: u6 = @intCast(((freqrange * 300) / 1000) + 1); + const trise: u6 = @intCast(((@as(u32, freqrange) * 300) / 1000) + 1); regs.RTR.write(.{ .TRISE = trise }); } @@ -156,6 +191,11 @@ pub const I2C = enum(u1) { .ACK = 1, // Enable ACK .ENGC = 0, // Disable general call }); + + // Enable DMA mode in I2C peripheral if configured + if (comptime config.dma != null) { + regs.CTLR2.modify(.{ .DMAEN = 1 }); + } } /// Disables I2C, returns peripheral registers to reset state. @@ -179,7 +219,7 @@ pub const I2C = enum(u1) { fn cleanup_stop(i2c: I2C) void { i2c.generate_stop(); - // Wait for BUSY to clear (with simple iteration timeout, ignore deadline) + // Wait for BUSY to clear i2c.wait_flag_star2("BUSY", 0, .no_deadline) catch {}; } @@ -407,4 +447,242 @@ pub const I2C = enum(u1) { try i2c.writev_blocking(addr, write_chunks, timeout); try i2c.readv_blocking(addr, read_chunks, timeout); } + + /// Write using DMA (only available if DMA configured) + pub fn write_dma( + i2c: I2C, + comptime config: Config, + addr: Address, + data: []const u8, + timeout: ?mdf.time.Duration, + ) Error!void { + comptime if (config.dma == null) + @compileError("write_dma() requires DMA configuration in I2C config"); + + const dma_cfg = comptime config.dma.?; + const regs = i2c.get_regs(); + const deadline = mdf.time.Deadline.init_relative(hal.time.get_time_since_boot(), timeout); + + if (data.len == 0) return Error.NoData; + + // Generate START condition + i2c.generate_start(); + errdefer i2c.cleanup_stop(); + + // Wait for SB (Start Bit) flag + try i2c.wait_flag_star1("SB", 1, deadline); + + // Send address with write bit + i2c.send_address(addr, .write); + + // Wait for ADDR flag + try i2c.wait_flag_star1("ADDR", 1, deadline); + + // Setup DMA transfer from memory to I2C DATAR + const peripheral_target = dma.PeripheralTarget{ + .addr = @intFromPtr(®s.DATAR), + }; + + dma_cfg.tx_channel.setup_transfer( + peripheral_target, // destination (I2C DATAR) + data, // source (memory buffer) + .{ + .priority = dma_cfg.priority, + .circular_mode = false, + }, + ); + + // Clear ADDR by reading SR2 (starts I2C and DMA transfer) + _ = regs.STAR2.read(); + + // Wait for DMA transfer completion + dma_cfg.tx_channel.wait_for_finish_blocking(); + + // Wait for BTF (Byte Transfer Finished) - ensures last byte transmitted + try i2c.wait_flag_star1("BTF", 1, deadline); + + // Generate STOP condition + i2c.generate_stop(); + + // Wait for BUSY flag to clear + try i2c.wait_flag_star2("BUSY", 0, deadline); + } + + /// Read using DMA (only available if DMA configured) + pub fn read_dma( + i2c: I2C, + comptime config: Config, + addr: Address, + dst: []u8, + timeout: ?mdf.time.Duration, + ) Error!void { + comptime if (config.dma == null) + @compileError("read_dma() requires DMA configuration in I2C config"); + + const dma_cfg = comptime config.dma.?; + const regs = i2c.get_regs(); + const deadline = mdf.time.Deadline.init_relative(hal.time.get_time_since_boot(), timeout); + + if (dst.len == 0) return Error.NoData; + + // Enable ACK + i2c.set_ack(true); + + // Generate START condition + i2c.generate_start(); + errdefer i2c.cleanup_stop(); + + // Wait for SB flag + try i2c.wait_flag_star1("SB", 1, deadline); + + // Send address with read bit + i2c.send_address(addr, .read); + + // Wait for ADDR flag + try i2c.wait_flag_star1("ADDR", 1, deadline); + + // CRITICAL: LAST tells I2C peripheral to NACK the final byte when DMA counter reaches 1 + // (DMAEN is already set globally in apply() method) + // This doesn't affect writes, so we can keep it set. + regs.CTLR2.modify(.{ .LAST = 1 }); + + // Setup DMA transfer from I2C DATAR to memory + const peripheral_target = dma.PeripheralTarget{ + .addr = @intFromPtr(®s.DATAR), + }; + + dma_cfg.rx_channel.setup_transfer( + dst, // write: destination (memory buffer) + peripheral_target, // read: source (I2C DATAR) + .{ + .priority = dma_cfg.priority, + .circular_mode = false, + }, + ); + + // Clear ADDR by reading SR2 (starts I2C and DMA transfer) + _ = regs.STAR2.read(); + + // Wait for DMA transfer completion + dma_cfg.rx_channel.wait_for_finish_blocking(); + + // Disable ACK and generate STOP + i2c.set_ack(false); + i2c.generate_stop(); + + // Wait for BUSY flag to clear + try i2c.wait_flag_star2("BUSY", 0, deadline); + } + + /// Automatic write - uses DMA if configured and transfer is large enough + /// + /// Automatically selects between DMA and polling based on transfer size. DMA is used for + /// transfers over configured threshold, polling for smaller transfers. + pub fn write_auto( + i2c: I2C, + comptime config: Config, + addr: Address, + data: []const u8, + timeout: ?mdf.time.Duration, + ) Error!void { + if (comptime config.dma) |dma_cfg| { + if (data.len >= dma_cfg.threshold) { + return i2c.write_dma(config, addr, data, timeout); + } + } + return i2c.write_blocking(addr, data, timeout); + } + + /// Automatic read - uses DMA if configured and transfer is large enough + /// + /// Automatically selects between DMA and polling based on transfer size. DMA is used for + /// transfers over configured threshold, polling for smaller transfers. + pub fn read_auto( + i2c: I2C, + comptime config: Config, + addr: Address, + dst: []u8, + timeout: ?mdf.time.Duration, + ) Error!void { + if (comptime config.dma) |dma_cfg| { + if (dst.len >= dma_cfg.threshold) { + return i2c.read_dma(config, addr, dst, timeout); + } + } + return i2c.read_blocking(addr, dst, timeout); + } + + /// Automatic vectored write - uses DMA per-chunk based on threshold + /// + /// For each chunk in the write operation: + /// - If chunk size >= DMA threshold: uses write_dma() + /// - If chunk size < DMA threshold: uses write_blocking() + /// + /// This approach maximizes DMA utilization without requiring buffer allocation. + /// When DMA is not configured, falls back to writev_blocking(). + pub fn writev_auto( + i2c: I2C, + comptime config: Config, + addr: Address, + chunks: []const []const u8, + timeout: ?mdf.time.Duration, + ) Error!void { + if (comptime config.dma) |dma_cfg| { + // Process each chunk individually, using DMA for chunks >= threshold + for (chunks) |chunk| { + if (chunk.len >= dma_cfg.threshold) { + try i2c.write_dma(config, addr, chunk, timeout); + } else { + try i2c.write_blocking(addr, chunk, timeout); + } + } + return; + } + return i2c.writev_blocking(addr, chunks, timeout); + } + + /// Automatic vectored read - uses DMA per-chunk based on threshold + /// + /// For each chunk in the read operation: + /// - If chunk size >= DMA threshold: uses read_dma() + /// - If chunk size < DMA threshold: uses read_blocking() + /// + /// This approach maximizes DMA utilization without requiring buffer allocation. + /// When DMA is not configured, falls back to readv_blocking(). + pub fn readv_auto( + i2c: I2C, + comptime config: Config, + addr: Address, + chunks: []const []u8, + timeout: ?mdf.time.Duration, + ) Error!void { + if (comptime config.dma) |dma_cfg| { + // Process each chunk individually, using DMA for chunks >= threshold + for (chunks) |chunk| { + if (chunk.len >= dma_cfg.threshold) { + try i2c.read_dma(config, addr, chunk, timeout); + } else { + try i2c.read_blocking(addr, chunk, timeout); + } + } + return; + } + return i2c.readv_blocking(addr, chunks, timeout); + } + + /// Automatic write-then-read operation + /// + /// Combines writev_auto() and readv_auto() to enable DMA for both operations. + /// Each operation independently selects DMA or polling based on chunk sizes. + pub fn writev_then_readv_auto( + i2c: I2C, + comptime config: Config, + addr: Address, + write_chunks: []const []const u8, + read_chunks: []const []u8, + timeout: ?mdf.time.Duration, + ) Error!void { + try i2c.writev_auto(config, addr, write_chunks, timeout); + try i2c.readv_auto(config, addr, read_chunks, timeout); + } };