From 7b7caa9eb40ca87db79958aeadd2e40155c1ab95 Mon Sep 17 00:00:00 2001 From: Matt Knight Date: Thu, 18 May 2023 20:51:23 -0700 Subject: [PATCH] improve ADC API (#62) --- examples/adc.zig | 12 +- src/hal/adc.zig | 386 +++++++++++++++++++++++++++++++---------------- 2 files changed, 262 insertions(+), 136 deletions(-) diff --git a/examples/adc.zig b/examples/adc.zig index b6d6bf4..ff180dd 100644 --- a/examples/adc.zig +++ b/examples/adc.zig @@ -7,7 +7,6 @@ const gpio = rp2040.gpio; const adc = rp2040.adc; const time = rp2040.time; -const temp_sensor: adc.Input = .temperature_sensor; const uart = rp2040.uart.num(0); const baud_rate = 115200; const uart_tx_pin = gpio.num(0); @@ -18,6 +17,10 @@ pub const std_options = struct { }; pub fn main() void { + adc.apply(.{ + .temp_sensor_enabled = true, + }); + uart.apply(.{ .baud_rate = baud_rate, .tx_pin = uart_tx_pin, @@ -26,9 +29,12 @@ pub fn main() void { }); rp2040.uart.init_logger(uart); - while (true) : (time.sleep_ms(1000)) { - const sample = temp_sensor.read(); + const sample = adc.convert_one_shot_blocking(.temp_sensor) catch { + std.log.err("conversion failed!", .{}); + continue; + }; + std.log.info("temp value: {}", .{sample}); } } diff --git a/src/hal/adc.zig b/src/hal/adc.zig index a77b102..592273b 100644 --- a/src/hal/adc.zig +++ b/src/hal/adc.zig @@ -7,196 +7,316 @@ const microzig = @import("microzig"); const ADC = microzig.chip.peripherals.ADC; const gpio = @import("gpio.zig"); const resets = @import("resets.zig"); +const clocks = @import("clocks.zig"); -pub const temperature_sensor = struct { - pub inline fn init() void { - set_temp_sensor_enabled(true); - } - - pub inline fn deinit() void { - set_temp_sensor_enabled(false); - } - - pub inline fn read_raw() u16 { - return Input.read(.temperature_sensor); - } - - // One-shot conversion returning the temperature in Celcius - pub inline fn read(comptime T: type, comptime Vref: T) T { - // TODO: consider fixed-point - const raw = @intToFloat(T, read_raw()); - const voltage: T = Vref * raw / 0x0fff; - return (27.0 - ((voltage - 0.706) / 0.001721)); - } +pub const Error = error{ + /// ADC conversion failed, one such reason is that the controller failed to + /// converge on a result. + Conversion, }; -pub const Input = enum(u3) { - ain0, - ain1, - ain2, - ain3, - temperature_sensor, - - /// Setup the GPIO pin as an ADC input - pub fn init(comptime input: Input) void { - switch (input) { - .temperature_sensor => set_temp_sensor_enabled(true), - else => { - const pin = gpio.num(@as(u5, @enumToInt(input)) + 26); - pin.set_function(.null); - - // TODO: implement these, otherwise adc isn't going to work. - //gpio.disablePulls(gpio_num); - //gpio.setInputEnabled(gpio_num, false); - }, - } - } - - /// Disables temp sensor, otherwise it does nothing if the input is - /// one of the others. - pub inline fn deinit(input: Input) void { - switch (input) { - .temperature_sensor => set_temp_sensor_enabled(true), - else => {}, - } - } - - /// Single-shot, blocking conversion - pub fn read(input: Input) u12 { - // TODO: not sure if setting these during the same write is - // correct - ADC.CS.modify(.{ - .AINSEL = @enumToInt(input), - .START_ONCE = 1, - }); - - // wait for the - while (ADC.CS.read().READY == 0) {} - - return ADC.RESULT.read().RESULT; - } -}; +/// temp_sensor is not valid because you can refer to it by name. +pub fn input(n: u2) Input { + return @intToEnum(Input, n); +} -pub const InputMask = InputMask: { - const enum_fields = @typeInfo(Input).Enum.fields; - var fields: [enum_fields.len]std.builtin.Type.StructField = undefined; - - const default_value: u1 = 0; - for (enum_fields, &fields) |enum_field, *field| - field = std.builtin.Type.StructField{ - .name = enum_field.name, - .field_type = u1, - .default_value = &default_value, - .is_comptime = false, - .alignment = 1, - }; +/// Enable the ADC controller. +pub fn set_enabled(enabled: bool) void { + ADC.CS.modify(.{ .EN = @boolToInt(enabled) }); +} - break :InputMask @Type(.{ - .Struct = .{ - .layout = .Packed, - .fields = &fields, - .backing_integer = std.meta.Int(.Unsigned, enum_fields.len), - .decls = &.{}, - .is_tuple = false, - }, - }); +const Config = struct { + /// Note that this frequency is the sample frequency of the controller, not + /// each input. So for 4 inputs in round-robin mode you'd see 1/4 sample + /// rate for a given put vs what is set here. + sample_frequency: ?u32 = null, + round_robin: ?InputMask = null, + fifo: ?fifo.Config = null, + temp_sensor_enabled: bool = false, }; -/// Initialize ADC hardware -pub fn init() void { +/// Applies configuration to ADC, leaves it in an enabled state by setting +/// CS.EN = 1. The global clock configuration is not needed to configure the +/// sample rate because the ADC hardware block requires a 48MHz clock. +pub fn apply(config: Config) void { ADC.CS.write(.{ - .EN = 1, - .TS_EN = 0, + .EN = 0, + .TS_EN = @boolToInt(config.temp_sensor_enabled), .START_ONCE = 0, .START_MANY = 0, .READY = 0, + .ERR = 0, .ERR_STICKY = 0, .AINSEL = 0, - .RROBIN = 0, + .RROBIN = if (config.round_robin) |rr| + @bitCast(u5, rr) + else + 0, .reserved8 = 0, .reserved12 = 0, .reserved16 = 0, .padding = 0, }); - while (ADC.CS.read().READY == 0) {} -} -/// Enable/disable ADC interrupt -pub inline fn irq_set_enabled(enable: bool) void { - // TODO: check if this works - ADC.INTE.write(.{ .FIFO = if (enable) @as(u1, 1) else @as(u1, 0) }); + if (config.sample_frequency) |sample_frequency| { + const cycles = (48_000_000 * 256) / @as(u64, sample_frequency); + ADC.DIV.write(.{ + .FRAC = @truncate(u8, cycles), + .INT = @intCast(u16, (cycles >> 8) - 1), + + .padding = 0, + }); + } + + if (config.fifo) |fifo_config| + fifo.apply(fifo_config); + + set_enabled(true); } /// Select analog input for next conversion. -pub inline fn select_input(input: Input) void { - ADC.CS.modify(.{ .AINSEL = @enumToInt(input) }); +pub fn select_input(in: Input) void { + ADC.CS.modify(.{ .AINSEL = @enumToInt(in) }); } /// Get the currently selected analog input. 0..3 are GPIO 26..29 respectively, /// 4 is the temperature sensor. -pub inline fn get_selected_input() Input { - // TODO: ensure that the field shouldn't have other values - return @intToEnum(Input, ADC.CS.read().AINSEL); +pub fn get_selected_input() Input { + const cs = ADC.SC.read(); + return @intToEnum(Input, cs.AINSEL); } +pub const Input = enum(u3) { + /// The temperature sensor must be enabled using + /// `set_temp_sensor_enabled()` in order to use it + temp_sensor = 5, + _, + + /// Get the corresponding GPIO pin for an ADC input. Panics if you give it + /// temp_sensor. + pub fn get_gpio_pin(in: Input) gpio.Pin { + return switch (in) { + else => gpio.num(@as(u5, @enumToInt(in)) + 26), + .temp_sensor => @panic("temp_sensor doesn't have a pin"), + }; + } + + /// Prepares an ADC input's corresponding GPIO pin to be used as an analog + /// input. + pub fn configure_gpio_pin(in: Input) void { + switch (in) { + else => { + const pin = in.get_gpio_pin(); + pin.set_function(.null); + pin.set_pull(null); + pin.set_input_enabled(false); + }, + .temp_sensor => {}, + } + } +}; + /// Set to true to power on the temperature sensor. -pub inline fn set_temp_sensor_enabled(enable: bool) void { - ADC.CS.modify(.{ .TS_EN = if (enable) @as(u1, 1) else @as(u1, 0) }); +pub fn set_temp_sensor_enabled(enable: bool) void { + ADC.CS.modify(.{ .TS_EN = @boolToInt(enable) }); +} + +/// T must be floating point. +pub fn temp_sensor_result_to_celcius(comptime T: type, comptime vref: T, result: u12) T { + // TODO: consider fixed-point + const raw = @intToFloat(T, result); + const voltage: T = vref * raw / 0x0fff; + return (27.0 - ((voltage - 0.706) / 0.001721)); } +/// For selecting which inputs are to be used in round-robin mode +pub const InputMask = packed struct(u5) { + ain0: bool = false, + ain1: bool = false, + ain2: bool = false, + ain3: bool = false, + temp_sensor: bool = false, +}; + /// Sets which of the inputs are to be run in round-robin mode. Setting all to /// 0 will disable round-robin mode but `disableRoundRobin()` is provided so /// the user may be explicit. -pub inline fn set_round_robin(comptime enabled_inputs: InputMask) void { +pub fn round_robin_set(enabled_inputs: InputMask) void { ADC.CS.modify(.{ .RROBIN = @bitCast(u5, enabled_inputs) }); } /// Disable round-robin sample mode. -pub inline fn disable_round_robin() void { +pub fn round_robin_disable() void { ADC.CS.modify(.{ .RROBIN = 0 }); } -/// Enable free-running sample mode. -pub inline fn run(enable: bool) void { - ADC.CS.modify(.{ .START_MANY = if (enable) @as(u1, 1) else @as(u1, 0) }); +pub const Mode = enum { + one_shot, + free_running, +}; + +/// Start the ADC controller. There are three "modes" that the controller +/// operates in: +/// +/// - one shot: the input is selected and then conversion is started. The +/// controller stops once the conversion is complete. +/// +/// - free running single input: the input is selected and then the conversion +/// is started. Once a conversion is complete the controller begins another +/// on the same input. +/// +/// - free running round-robin: a mask of which inputs to sample is set using +/// `round_robin_set()`. Once conversion is completed for one input, a +/// conversion is started for the next set input in the mask. +pub fn start(mode: Mode) void { + switch (mode) { + .one_shot => ADC.CS.modify(.{ + .START_ONCE = 1, + }), + .free_running => ADC.CS.modify(.{ + .START_MANY = 1, + }), + } +} + +/// Check whether the ADC controller has a conversion result +pub fn is_ready() bool { + const cs = ADC.CS.read(); + return cs.READY != 0; } -pub inline fn set_clk_div() void { - @compileError("todo"); +/// Single-shot, blocking conversion +pub fn convert_one_shot_blocking(in: Input) Error!u12 { + select_input(in); + start(.one_shot); + + while (!is_ready()) {} + + return read_result(); } -/// The fifo is 4 samples long, if a conversion is completed and the FIFO is -/// full, the result is dropped. +/// Read conversion result from ADC controller, this function assumes that the +/// controller has a result ready. +pub fn read_result() Error!u12 { + const cs = ADC.CS.read(); + return if (cs.ERR == 1) + error.Conversion + else blk: { + const conversion = ADC.RESULT.read(); + break :blk conversion.RESULT; + }; +} + +/// The ADC FIFO can store up to four conversion results. It must be enabled in +/// order to use DREQ or IRQ driven streaming. pub const fifo = struct { - pub inline fn setup() void { - @compileError("todo"); - // There are a number of considerations wrt DMA and error detection + // TODO: what happens when DMA and IRQ are enabled? + pub const Config = struct { + /// Assert DMA requests when the fifo contains data + dreq_enabled: bool = false, + /// Assert Interrupt when fifo contains data + irq_enabled: bool = false, + /// DREQ/IRQ asserted when level >= threshold + thresh: u4 = 0, + /// Shift the conversion so it's 8-bit, good for DMAing to a byte + /// buffer + shift: bool = false, + }; + + /// Apply ADC FIFO configuration and enable it + pub fn apply(config: fifo.Config) void { + ADC.FCS.write(.{ + .DREQ_EN = @boolToInt(config.dreq_enabled), + .THRESH = config.thresh, + .SHIFT = @boolToInt(config.shift), + + .EN = 1, + .EMPTY = 0, + .FULL = 0, + .LEVEL = 0, + + // As far as it is known, there is zero cost to being able to + // report errors in the FIFO, so let's. + .ERR = 1, + + // Writing 1 to these will clear them if they're already set + .UNDER = 1, + .OVER = 1, + + .reserved8 = 0, + .reserved16 = 0, + .reserved24 = 0, + .padding = 0, + }); + + irq_set_enabled(config.irq_enabled); + } + + // TODO: do we need to acknowledge an ADC interrupt? + + /// Enable/disable ADC interrupt. + pub fn irq_set_enabled(enable: bool) void { + // TODO: check if this works + ADC.INTE.write(.{ + .FIFO = @boolToInt(enable), + .padding = 0, + }); + } + + /// Check if the ADC FIFO is full. + pub fn is_full() bool { + const fsc = ADC.FSC.read(); + return fsc.FULL != 0; + } + + /// Check if the ADC FIFO is empty. + pub fn is_empty() bool { + const fsc = ADC.FSC.read(); + return fsc.EMPTY != 0; + } + + /// Get the number of conversion in the ADC FIFO. + pub fn get_level() u4 { + const fsc = ADC.FSC.read(); + return fsc.LEVEL; } - /// Return true if FIFO is empty. - pub inline fn is_empty() bool { - @compileError("todo"); + /// Check if the ADC FIFO has overflowed. When overflow happens, the new + /// conversion is discarded. This flag is sticky, to clear it call + /// `clear_overflowed()`. + pub fn has_overflowed() bool { + const fsc = ADC.FSC.read(); + return fsc.OVER != 0; } - /// Read how many samples are in the FIFO. - pub inline fn get_level() u8 { - @compileError("todo"); + /// Clear the overflow status flag if it is set. + pub fn clear_overflowed() void { + ADC.FSC.modify(.{ .OVER = 1 }); } - /// Pop latest result from FIFO. - pub inline fn get() u16 { - @compileError("todo"); + /// Check if the ADC FIFO has underflowed. This means that the FIFO + /// register was read while the FIFO was empty. This flag is sticky, to + /// clear it call `clear_underflowed()`. + pub fn has_underflowed() bool { + const fsc = ADC.FSC.read(); + return fsc.UNDER != 0; } - /// Block until result is available in FIFO, then pop it. - pub inline fn get_blocking() u16 { - @compileError("todo"); + /// Clear the underflow status flag if it is set. + pub fn clear_underflowed() void { + ADC.FSC.modify(.{ .UNDER = 1 }); } - /// Wait for conversion to complete then discard results in FIFO. - pub inline fn drain() void { - @compileError("todo"); + /// Pop conversion from ADC FIFO. This function assumes that the FIFO is + /// not empty. + pub fn pop() Error!u12 { + assert(!is_empty()); + const result = ADC.FIFO.read(); + return if (result.ERR == 1) + error.Conversion + else + result.VAL; } };