PWM measurement and generation
Four GPIO pins can measure an external PWM signal or generate a square wave. These pins belong to the same two channels as the SPI, I2C and UART interfaces, so PWM follows the same sharing rules as those interfaces.
Pin roles​
The role of each pin is fixed by the hardware:
| GPIO | Role | Channel |
|---|---|---|
| DA17 | PWM input (measurement) | 0 |
| DA18 | PWM output (square wave, about 50 % duty) | 0 |
| DA21 | PWM input (measurement) | 1 |
| DA22 | PWM output (square wave, about 50 % duty) | 1 |
Any other pin is rejected with a 400 error. A PWM handle is obtained with gpio.pwm(io):
- NodeJS
- Python
- Rust
const pwm_in = tester.gpio.pwm(17);
const pwm_out = tester.gpio.pwm(18);
pwm_in = tester.gpio.pwm(17)
pwm_out = tester.gpio.pwm(18)
let pwm_in = tester.gpio.pwm(17)?;
let pwm_out = tester.gpio.pwm(18)?;
Channel sharing​
Each channel serves one function at a time. Channel 0 is DA16 to DA19 and channel 1 is DA20 to DA23 (see pin alternate functions). The two PWM pins of a channel are DA17 with DA18 and DA21 with DA22.
- Enabling PWM fails with
409 CONFIGURATION_CONFLICTwhile I2C, SPI or UART is enabled on the same channel, or while the other PWM pin of that channel is enabled. - In the other direction, enabling I2C, SPI or UART on the channel, or configuring the pin as a GPIO output, fails with
409 CONFIGURATION_CONFLICTwhile PWM is enabled. POST /com/reset(tester.com.reset()) leaves PWM running.POST /gpio/reset(tester.gpio.reset()) stops it and restores the PWM defaults.
DA18 and DA21 are on different channels, so an output on DA18 and an input on DA21 can run at the same time.
Supply voltage (vcc)​
vcc sets the target supply of the pin's level shifter, from 1.8 V to 3.3 V inclusive. The default is 3.3 V.
- On an output,
vccis the high level driven on the pin. - On an input,
vccis the highest voltage the pin accepts.
vcc does not move the input switching thresholds. They stay at about 0.8 V to 1.35 V.
Pin-voltage safety check​
Enabling PWM, or changing vcc while PWM runs, first measures the pin. If it reads below −0.5 V or above vcc + 0.5 V, the request fails with 409 UNSAFE_PIN_VOLTAGE and PWM stays disabled. The error message gives the voltage measured and the allowed window.
Generating a square wave​
An output pin generates a square wave of about 50 % duty cycle.
- Requested frequencies range from 91.55 Hz to 7.5 MHz.
- The device generates the closest average frequency it can produce and returns it in the
frequencyfield of the configuration. Read this value back rather than assuming the requested one. - The generated frequency is 8 × 30 MHz / (8d + 9), or 8 × 6 MHz / (8d + 9) below about 458 Hz, where d is an integer divisor.
- Every eighth low phase is one clock period longer than the others. The average frequency therefore sits slightly below round requests (about −0.4 % at 1 MHz), and the duty cycle is slightly under 50 % (49.8 % at 1 MHz, 49 % at 5 MHz).
The output runs until it is disabled, until POST /gpio/reset, or until duration seconds have passed. duration is in (0, 86400] s. Without a duration, the output runs until it is disabled.
Changing the frequency or duration of a running output restarts it. The pin's level shifter is off for about 100 ms during the restart, and duration counts from the change.
The following example generates 1 kHz at 3.3 V on DA18 for 10 s and prints the frequency actually generated:
- NodeJS
- Python
- Rust
const pwm_out = tester.gpio.pwm(18);
const config = await pwm_out.configure_output({ frequency: 1000, vcc: 3.3, duration: 10 });
console.log(`Generating ${config.frequency} Hz`);
pwm_out = tester.gpio.pwm(18)
config = pwm_out.configure_output(frequency=1000, vcc=3.3, duration=10)
print(f"Generating {config.frequency} Hz")
use ikalogic_at1000::PwmOutputConfig;
let pwm_out = tester.gpio.pwm(18)?;
let config = pwm_out.configure_output(&PwmOutputConfig {
frequency: 1000.0,
vcc: Some(3.3),
duration: Some(10.0),
})?;
println!("Generating {:?} Hz", config.frequency);
Measuring a PWM signal​
An input pin is sampled at 30 MHz. Each call to measure() runs one capture:
- The capture runs until it holds at least one full cycle (rising, falling, then rising edge) and 100 ms have passed. It ends on a rising edge.
timeout(default 5 s, range 0.1 s to 60 s) ends the capture whatever edges were seen.statusis"ok"when at least one full cycle was captured, otherwise"timeout".- Only
"ok"results carry the statistics (frequency, duty cycle, period and high/low times). They cover whole cycles from the first to the last captured rising edge. - Every result carries
level, the line level at the end of the capture, plusedges,cyclesandduration.
Signals up to 5 MHz are measured accurately. Above about 6 MHz (pulses shorter than about 67 ns), the samples alias silently and the result is wrong without any error.
If the AT1000 could not keep up with a capture, the request returns a 500 error. Retry the measurement.
The following example enables measurement on DA17 at 3.3 V and runs a capture of at most 2 s:
- NodeJS
- Python
- Rust
const pwm_in = tester.gpio.pwm(17);
await pwm_in.configure_input({ vcc: 3.3 });
const m = await pwm_in.measure({ timeout: 2 });
if (m.status === 'ok') {
console.log(`Frequency: ${m.frequency} Hz, duty cycle: ${m.duty_cycle} %`);
} else {
console.log(`No full cycle within 2 s, line is ${m.level ? 'high' : 'low'}`);
}
pwm_in = tester.gpio.pwm(17)
pwm_in.configure_input(vcc=3.3)
m = pwm_in.measure(timeout=2)
if m.status == "ok":
print(f"Frequency: {m.frequency} Hz, duty cycle: {m.duty_cycle} %")
else:
print(f"No full cycle within 2 s, line is {'high' if m.level else 'low'}")
use ikalogic_at1000::{PwmInputConfig, PwmStatus};
let pwm_in = tester.gpio.pwm(17)?;
pwm_in.configure_input(&PwmInputConfig { vcc: Some(3.3) })?;
let m = pwm_in.measure(Some(2.0))?;
match m.status {
PwmStatus::Ok => println!(
"Frequency: {:?} Hz, duty cycle: {:?} %",
m.frequency, m.duty_cycle
),
PwmStatus::Timeout => println!(
"No full cycle within 2 s, line is {}",
if m.level { "high" } else { "low" }
),
}
Loopback test​
With DA18 wired to DA21, the AT1000 can measure its own output. The two pins are on different channels, so they do not conflict.
- NodeJS
- Python
- Rust
const pwm_out = tester.gpio.pwm(18);
const pwm_in = tester.gpio.pwm(21);
const config = await pwm_out.configure_output({ frequency: 100000, vcc: 3.3 });
await pwm_in.configure_input({ vcc: 3.3 });
const m = await pwm_in.measure();
if (m.status === 'ok') {
console.log(`Generated ${config.frequency} Hz, measured ${m.frequency} Hz, duty ${m.duty_cycle} %`);
}
await pwm_out.disable();
await pwm_in.disable();
pwm_out = tester.gpio.pwm(18)
pwm_in = tester.gpio.pwm(21)
config = pwm_out.configure_output(frequency=100000, vcc=3.3)
pwm_in.configure_input(vcc=3.3)
m = pwm_in.measure()
if m.status == "ok":
print(f"Generated {config.frequency} Hz, measured {m.frequency} Hz, duty {m.duty_cycle} %")
pwm_out.disable()
pwm_in.disable()
use ikalogic_at1000::{PwmInputConfig, PwmOutputConfig, PwmStatus};
let pwm_out = tester.gpio.pwm(18)?;
let pwm_in = tester.gpio.pwm(21)?;
let config = pwm_out.configure_output(&PwmOutputConfig {
frequency: 100_000.0,
vcc: Some(3.3),
duration: None,
})?;
pwm_in.configure_input(&PwmInputConfig { vcc: Some(3.3) })?;
let m = pwm_in.measure(None)?;
if m.status == PwmStatus::Ok {
println!(
"Generated {:?} Hz, measured {:?} Hz, duty {:?} %",
config.frequency, m.frequency, m.duty_cycle
);
}
pwm_out.disable()?;
pwm_in.disable()?;
Handling a channel conflict​
I2C 0 uses channel 0, so PWM on DA17 cannot be enabled while I2C 0 is enabled. The device answers with the code CONFIGURATION_CONFLICT:
- NodeJS
- Python
- Rust
import { ApiError } from '@ikalogic/at1000';
await tester.com.i2c(0).enable({ baud_rate: 400000, vcc: 3.3 });
try {
await tester.gpio.pwm(17).configure_input({ vcc: 3.3 });
} catch (err) {
if (err instanceof ApiError && err.code === 'CONFIGURATION_CONFLICT') {
console.log(`Channel busy: ${err.detail}`);
} else {
throw err;
}
}
from ikalogic_at1000 import ApiError
tester.com.i2c(0).enable(baud_rate=400000, vcc=3.3)
try:
tester.gpio.pwm(17).configure_input(vcc=3.3)
except ApiError as err:
if err.code != "CONFIGURATION_CONFLICT":
raise
print(f"Channel busy: {err.detail}")
use ikalogic_at1000::{At1000Error, I2cEnableConfig, PwmInputConfig};
tester.com.i2c(0)?.enable(&I2cEnableConfig {
baud_rate: Some(400_000),
vcc: Some(3.3),
})?;
match tester.gpio.pwm(17)?.configure_input(&PwmInputConfig { vcc: Some(3.3) }) {
Err(At1000Error::Api(err)) if err.code == "CONFIGURATION_CONFLICT" => {
println!("Channel busy: {}", err.message);
}
Err(err) => return Err(err),
Ok(_) => {}
}
UNSAFE_PIN_VOLTAGE is handled the same way.
Realtime event​
Every change to a PWM configuration emits a gpio.pwm_config event. Its id is the pin (17, 18, 21 or 22) and its data is the PWM configuration. The event is also emitted when a timed output ends. See realtime events.
REST API​
The SDK methods map to three endpoints. Requests that change the device require the session token.
| Method | Path | Body | Response | Session token |
|---|---|---|---|---|
GET | /v0/gpio/{io_id}/pwm/config | - | IOPwmConfig | not required |
PATCH | /v0/gpio/{io_id}/pwm/config | IOPwmConfigPatch | IOPwmConfig | required |
POST | /v0/gpio/{io_id}/pwm/measure | IOPwmMeasureRequest, may be {} | IOPwmMeasurement | required |
io_id is 17 or 21 for an input and 18 or 22 for an output. measure accepts only 17 and 21.
IOPwmConfig​
| Field | Type | Description |
|---|---|---|
enabled | boolean | Whether PWM is enabled on the pin |
direction | "input" | "output" | Role of the pin |
vcc | number | Level-shifter supply in volts, 1.8 to 3.3 |
frequency | number, optional | Outputs only: the frequency generated, in Hz, after quantization |
duration | number, optional | Outputs only: the run time in seconds requested at the last start. Absent when the output runs until disabled |
{ "enabled": true, "direction": "output", "vcc": 3.3, "frequency": 999.9958333506944, "duration": 10 }
IOPwmConfigPatch​
All fields are optional. Omitted fields keep their value.
| Field | Type | Description |
|---|---|---|
enabled | boolean | Enables or disables PWM |
vcc | number | 1.8 to 3.3 V |
frequency | number | Outputs only: requested frequency, 91.55 Hz to 7.5 MHz |
duration | number | null | Outputs only: run time in seconds, in (0, 86400], counted from this request. null clears it, so the output runs until disabled |
{ "enabled": true, "vcc": 3.3, "frequency": 1000, "duration": 10 }
IOPwmMeasureRequest​
| Field | Type | Description |
|---|---|---|
timeout | number, optional | Longest capture in seconds, 0.1 to 60. Default 5 |
IOPwmMeasurement​
The result is a discriminated union on status. All times are in seconds.
| Field | Present | Description |
|---|---|---|
status | always | "ok" when at least one full cycle was captured, otherwise "timeout" |
level | always | Line level at the end of the capture |
edges | always | Number of edges captured |
cycles | always | Number of whole cycles captured |
duration | always | Captured time |
frequency | "ok" only | Average frequency in Hz |
duty_cycle | "ok" only | Duty cycle in percent |
period, period_min, period_max | "ok" only | Average, shortest and longest period |
high_time, high_time_min, high_time_max | "ok" only | Average, shortest and longest high time |
low_time, low_time_min, low_time_max | "ok" only | Average, shortest and longest low time |
{ "status": "timeout", "level": false, "edges": 0, "cycles": 0, "duration": 2.0 }
Errors​
Errors use the standard { code, message, suggestion, path } body.
| HTTP | code | When it fires |
|---|---|---|
| 400 | INVALID_VALUE | The pin does not support PWM or does not have this role, a value is out of range, or frequency/duration is sent for an input |
| 400 | INVALID_CONFIGURATION | measure was called while PWM is disabled on the pin |
| 409 | CONFIGURATION_CONFLICT | I2C, SPI, UART or the other PWM pin is enabled on the same channel (see channel sharing) |
| 409 | UNSAFE_PIN_VOLTAGE | The pin failed the voltage safety check |
| 500 | INTERNAL_ERROR | The AT1000 could not keep up with the capture. Retry the measurement |
The full method reference is in the GPIO API reference.