Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions examples/wch/ch32v/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
81 changes: 81 additions & 0 deletions examples/wch/ch32v/src/i2c_position_sensor.zig
Original file line number Diff line number Diff line change
@@ -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);
}
}
12 changes: 11 additions & 1 deletion port/wch/ch32v/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
176 changes: 175 additions & 1 deletion port/wch/ch32v/src/hals/dma.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]);
}
Loading
Loading