Skip to main content

pamoja_ffi/
actuators_driver.rs

1//! The C ABI for the actuator drivers that run over an I2C bus.
2//!
3//! A [`PamojaPca9685`] drives the PCA9685's sixteen PWM channels over a [`PamojaI2cBus`]. It
4//! puts the oscillator to sleep, writes the prescale for its frequency, sets the output
5//! wiring, wakes the oscillator, waits the 500 us it needs, and restarts the channels; then it
6//! loads a channel's four registers in one transfer. It holds a share of the bus, so the
7//! caller may free its own bus handle straight after building the driver, or keep it to read
8//! the part back.
9//!
10//! [`pamoja_pca9685_sim_part`] is a PCA9685 that is not there. It holds the part's power-on
11//! registers and keeps its datasheet's rules, so what a driver wrote reads back off a
12//! simulated bus the way the part would hold it.
13//!
14//! A failure is [`PamojaStatus::Io`] when the bus failed and
15//! [`PamojaStatus::InvalidArgument`] for a command the part cannot take, such as a channel past
16//! 15, with the reason in the last error message.
17
18use std::fmt::{Debug, Display};
19use std::panic::{catch_unwind, AssertUnwindSafe};
20
21use pamoja_actuators::pca9685::{self, Outputs, Pwm};
22use pamoja_actuators::DriverError;
23use pamoja_hal::bus::{BusDelay, I2cBus};
24
25use crate::actuators::PamojaPwm;
26use crate::hal::{PamojaI2cBus, PamojaI2cPart};
27use crate::{set_last_error, PamojaStatus};
28
29type Pca9685Driver = pca9685::Pca9685<I2cBus, BusDelay>;
30
31/// The frequency a PCA9685 driver runs at unless given another, in hertz: the part's own
32/// power-on 200 Hz.
33pub const PAMOJA_PCA9685_DEFAULT_FREQUENCY_HZ: u32 = 200;
34
35/// How a PCA9685's sixteen outputs are wired, the MODE2 register.
36#[repr(C)]
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub struct PamojaPca9685Outputs {
39    /// 1 for totem-pole outputs, the power-on setting; 0 for open-drain.
40    pub totem_pole: u8,
41    /// 1 to invert the output logic, for a board with no external driver.
42    pub inverted: u8,
43    /// 1 to change the outputs on the acknowledge of each register write rather than on the
44    /// stop condition.
45    pub change_on_ack: u8,
46}
47
48/// How a PCA9685 driver programs the part.
49#[repr(C)]
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub struct PamojaPca9685Settings {
52    /// The PWM frequency every channel shares, in hertz; the prescaler reaches 24 to 1526 at
53    /// 25 MHz, and 50 is what a hobby servo wants.
54    pub frequency_hz: u32,
55    /// The clock the prescaler divides, in hertz: the internal 25 MHz unless the board drives
56    /// EXTCLK.
57    pub oscillator_hz: u32,
58    /// How the outputs are wired.
59    pub outputs: PamojaPca9685Outputs,
60}
61
62/// A PCA9685 driver. Opaque; release it with [`pamoja_pca9685_free`].
63pub struct PamojaPca9685 {
64    driver: Pca9685Driver,
65}
66
67impl From<PamojaPca9685Outputs> for Outputs {
68    fn from(value: PamojaPca9685Outputs) -> Self {
69        Outputs {
70            totem_pole: value.totem_pole != 0,
71            inverted: value.inverted != 0,
72            change_on_ack: value.change_on_ack != 0,
73        }
74    }
75}
76
77impl From<Outputs> for PamojaPca9685Outputs {
78    fn from(value: Outputs) -> Self {
79        PamojaPca9685Outputs {
80            totem_pole: u8::from(value.totem_pole),
81            inverted: u8::from(value.inverted),
82            change_on_ack: u8::from(value.change_on_ack),
83        }
84    }
85}
86
87/// Returns the settings a PCA9685 driver takes when given none: 200 Hz on the internal
88/// oscillator with totem-pole outputs, the part's own power-on state.
89///
90/// # Returns
91///
92/// The settings.
93#[no_mangle]
94pub extern "C" fn pamoja_pca9685_settings_default() -> PamojaPca9685Settings {
95    PamojaPca9685Settings {
96        frequency_hz: PAMOJA_PCA9685_DEFAULT_FREQUENCY_HZ,
97        oscillator_hz: pca9685::INTERNAL_OSC_HZ,
98        outputs: Outputs::default().into(),
99    }
100}
101
102/// Creates a PCA9685 driver on a bus. Nothing is sent until [`pamoja_pca9685_init`] or the
103/// first channel is loaded.
104///
105/// # Arguments
106///
107/// * `bus` - the bus the part is on; the driver holds its own share.
108/// * `address` - the address the A5 to A0 pins select, `0x40` with all six low.
109/// * `settings` - the frequency, the oscillator, and the output wiring; see
110///   [`pamoja_pca9685_settings_default`].
111/// * `out_driver` - receives the driver.
112///
113/// # Returns
114///
115/// [`PamojaStatus::Ok`] with the driver in `out_driver`, or [`PamojaStatus::InvalidArgument`]
116/// for a null argument.
117///
118/// # Safety
119///
120/// `bus` must be a live handle or null, and `out_driver` a writable pointer or null.
121#[no_mangle]
122pub unsafe extern "C" fn pamoja_pca9685_new(
123    bus: *const PamojaI2cBus,
124    address: u8,
125    settings: PamojaPca9685Settings,
126    out_driver: *mut *mut PamojaPca9685,
127) -> PamojaStatus {
128    if out_driver.is_null() {
129        set_last_error("out_driver must not be null".to_owned());
130        return PamojaStatus::InvalidArgument;
131    }
132    *out_driver = std::ptr::null_mut();
133    let Some(bus) = bus.as_ref() else {
134        set_last_error("bus must not be null".to_owned());
135        return PamojaStatus::InvalidArgument;
136    };
137    let bus = bus.bus.clone();
138    let delay = bus.delay();
139    let driver = pca9685::Pca9685::new(bus, address, delay)
140        .with_frequency(settings.frequency_hz)
141        .with_oscillator(settings.oscillator_hz)
142        .with_outputs(settings.outputs.into());
143    *out_driver = Box::into_raw(Box::new(PamojaPca9685 { driver }));
144    PamojaStatus::Ok
145}
146
147/// Puts the oscillator to sleep, writes the prescale and the output wiring, wakes it, waits
148/// the 500 us it needs, and restarts the channels.
149///
150/// # Returns
151///
152/// [`PamojaStatus::Ok`], [`PamojaStatus::Io`] when the bus failed, or
153/// [`PamojaStatus::InvalidArgument`] for a null driver.
154///
155/// # Safety
156///
157/// `driver` must be a live handle or null.
158#[no_mangle]
159pub unsafe extern "C" fn pamoja_pca9685_init(driver: *mut PamojaPca9685) -> PamojaStatus {
160    run(driver, |held| held.driver.init())
161}
162
163/// Loads one channel's on and off counts, initializing the part first if it has not been.
164///
165/// # Arguments
166///
167/// * `driver` - the driver.
168/// * `channel` - the output, 0 to 15.
169/// * `pwm` - the four register bytes to load, as the `pamoja_pwm_` functions build them.
170///
171/// # Returns
172///
173/// [`PamojaStatus::Ok`], [`PamojaStatus::InvalidArgument`] for a channel past 15 or a null
174/// driver, or [`PamojaStatus::Io`] when the bus failed.
175///
176/// # Safety
177///
178/// `driver` must be a live handle or null.
179#[no_mangle]
180pub unsafe extern "C" fn pamoja_pca9685_set_channel(
181    driver: *mut PamojaPca9685,
182    channel: u8,
183    pwm: PamojaPwm,
184) -> PamojaStatus {
185    run(driver, |held| held.driver.set_channel(channel, pwm_of(pwm)))
186}
187
188/// Reads one channel's counts back from the part, one register a transfer, so the read works
189/// whatever MODE1 holds and changes nothing on the part.
190///
191/// # Arguments
192///
193/// * `driver` - the driver.
194/// * `channel` - the output, 0 to 15.
195/// * `out_pwm` - receives the four register bytes the channel holds.
196///
197/// # Returns
198///
199/// As [`pamoja_pca9685_set_channel`], and [`PamojaStatus::InvalidArgument`] for a null
200/// `out_pwm`.
201///
202/// # Safety
203///
204/// `driver` must be a live handle or null, and `out_pwm` a writable pointer or null.
205#[no_mangle]
206pub unsafe extern "C" fn pamoja_pca9685_channel(
207    driver: *mut PamojaPca9685,
208    channel: u8,
209    out_pwm: *mut PamojaPwm,
210) -> PamojaStatus {
211    let Some(out) = out_pwm.as_mut() else {
212        set_last_error("out_pwm must not be null".to_owned());
213        return PamojaStatus::InvalidArgument;
214    };
215    run(driver, |held| {
216        *out = held.driver.channel(channel)?.into();
217        Ok(())
218    })
219}
220
221/// Loads every channel with the same counts in one transfer, through the ALL_LED registers.
222///
223/// # Arguments
224///
225/// * `driver` - the driver.
226/// * `pwm` - the four register bytes to load.
227///
228/// # Returns
229///
230/// As [`pamoja_pca9685_set_channel`].
231///
232/// # Safety
233///
234/// `driver` must be a live handle or null.
235#[no_mangle]
236pub unsafe extern "C" fn pamoja_pca9685_set_all(
237    driver: *mut PamojaPca9685,
238    pwm: PamojaPwm,
239) -> PamojaStatus {
240    run(driver, |held| held.driver.set_all(pwm_of(pwm)))
241}
242
243/// Stops the oscillator. The channel registers keep their values, and a channel still
244/// running when the part sleeps sets its RESTART bit.
245///
246/// # Returns
247///
248/// As [`pamoja_pca9685_init`].
249///
250/// # Safety
251///
252/// `driver` must be a live handle or null.
253#[no_mangle]
254pub unsafe extern "C" fn pamoja_pca9685_sleep(driver: *mut PamojaPca9685) -> PamojaStatus {
255    run(driver, |held| held.driver.sleep())
256}
257
258/// Wakes the oscillator, waits the 500 us it needs, and restarts every channel that was
259/// running before the sleep.
260///
261/// # Returns
262///
263/// As [`pamoja_pca9685_init`].
264///
265/// # Safety
266///
267/// `driver` must be a live handle or null.
268#[no_mangle]
269pub unsafe extern "C" fn pamoja_pca9685_wake(driver: *mut PamojaPca9685) -> PamojaStatus {
270    run(driver, |held| held.driver.wake())
271}
272
273/// Sends the general-call software reset, which returns every PCA9685 on the bus to its
274/// power-on state, not only this one. It goes to address `0x00`, which nothing on a simulated
275/// bus answers.
276///
277/// # Returns
278///
279/// As [`pamoja_pca9685_init`].
280///
281/// # Safety
282///
283/// `driver` must be a live handle or null.
284#[no_mangle]
285pub unsafe extern "C" fn pamoja_pca9685_software_reset(driver: *mut PamojaPca9685) -> PamojaStatus {
286    run(driver, |held| held.driver.software_reset())
287}
288
289/// Returns the prescale value the driver writes for its frequency.
290///
291/// # Returns
292///
293/// The PRE_SCALE register value, or 0 for a null driver.
294///
295/// # Safety
296///
297/// `driver` must be a live handle or null.
298#[no_mangle]
299pub unsafe extern "C" fn pamoja_pca9685_prescale(driver: *const PamojaPca9685) -> u8 {
300    driver.as_ref().map_or(0, |held| held.driver.prescale())
301}
302
303/// Returns the frequency the part runs at once the prescaler has rounded the one asked for.
304///
305/// # Returns
306///
307/// The frequency in hertz, or 0 for a null driver.
308///
309/// # Safety
310///
311/// `driver` must be a live handle or null.
312#[no_mangle]
313pub unsafe extern "C" fn pamoja_pca9685_frequency(driver: *const PamojaPca9685) -> f32 {
314    driver.as_ref().map_or(0.0, |held| held.driver.frequency())
315}
316
317/// Releases a driver and its share of the bus. A null pointer is ignored.
318///
319/// # Safety
320///
321/// `driver` must be a handle from [`pamoja_pca9685_new`] that has not been freed, or null.
322#[no_mangle]
323pub unsafe extern "C" fn pamoja_pca9685_free(driver: *mut PamojaPca9685) {
324    if !driver.is_null() {
325        drop(Box::from_raw(driver));
326    }
327}
328
329/// Creates a simulated PCA9685 as it powers up: asleep at 200 Hz with every output off,
330/// keeping its datasheet's rules for writes, reads, and its register pointer.
331///
332/// # Arguments
333///
334/// * `address` - the address it answers to.
335///
336/// # Returns
337///
338/// A part to put on a simulated bus, released with
339/// [`pamoja_i2c_part_free`](crate::hal::pamoja_i2c_part_free).
340#[no_mangle]
341pub extern "C" fn pamoja_pca9685_sim_part(address: u8) -> *mut PamojaI2cPart {
342    PamojaI2cPart::into_raw(pca9685::sim::part(address))
343}
344
345fn pwm_of(pwm: PamojaPwm) -> Pwm {
346    Pwm::from_bytes(&[pwm.on_low, pwm.on_high, pwm.off_low, pwm.off_high])
347}
348
349/// Runs a driver call, turning a failure or a panic into a status.
350///
351/// # Safety
352///
353/// `driver` must be a live handle or null.
354unsafe fn run<E: Debug + Display>(
355    driver: *mut PamojaPca9685,
356    call: impl FnOnce(&mut PamojaPca9685) -> Result<(), DriverError<E>>,
357) -> PamojaStatus {
358    let Some(held) = driver.as_mut() else {
359        set_last_error("driver must not be null".to_owned());
360        return PamojaStatus::InvalidArgument;
361    };
362    match catch_unwind(AssertUnwindSafe(|| call(held))) {
363        Ok(Ok(())) => PamojaStatus::Ok,
364        Ok(Err(DriverError::Command(reason))) => {
365            set_last_error(reason.to_owned());
366            PamojaStatus::InvalidArgument
367        }
368        Ok(Err(DriverError::Bus(error))) => {
369            set_last_error(error.to_string());
370            PamojaStatus::Io
371        }
372        Err(_) => {
373            set_last_error("panic at the FFI boundary".to_owned());
374            PamojaStatus::Panic
375        }
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382    use crate::hal::{
383        pamoja_i2c_bus_attach, pamoja_i2c_bus_free, pamoja_i2c_bus_part, pamoja_i2c_bus_simulated,
384        pamoja_i2c_bus_waited_micros, pamoja_i2c_part_free, pamoja_i2c_part_register,
385    };
386    use crate::{actuators, pamoja_last_error_message};
387    use std::ffi::CStr;
388    use std::ptr;
389
390    fn last_error() -> String {
391        unsafe { CStr::from_ptr(pamoja_last_error_message()) }
392            .to_string_lossy()
393            .into_owned()
394    }
395
396    #[test]
397    fn a_driver_programs_a_simulated_part_as_the_datasheet_asks() {
398        unsafe {
399            let part = pamoja_pca9685_sim_part(0x40);
400            let bus = pamoja_i2c_bus_simulated();
401            assert_eq!(pamoja_i2c_bus_attach(bus, part), PamojaStatus::Ok);
402            pamoja_i2c_part_free(part);
403
404            let mut settings = pamoja_pca9685_settings_default();
405            assert_eq!(settings.frequency_hz, 200);
406            assert_eq!(settings.outputs.totem_pole, 1);
407            settings.frequency_hz = 50;
408            let mut driver = ptr::null_mut();
409            assert_eq!(
410                pamoja_pca9685_new(bus, 0x40, settings, &mut driver),
411                PamojaStatus::Ok
412            );
413            assert_eq!(pamoja_pca9685_prescale(driver), 121);
414            assert!((pamoja_pca9685_frequency(driver) - 50.0).abs() < 0.1);
415
416            let servo = actuators::pamoja_pwm_servo(1_500, 50);
417            assert_eq!(
418                pamoja_pca9685_set_channel(driver, 0, servo),
419                PamojaStatus::Ok
420            );
421            assert_eq!(
422                pamoja_pca9685_set_channel(driver, 16, servo),
423                PamojaStatus::InvalidArgument
424            );
425            assert!(
426                last_error().contains("sixteen channels"),
427                "{}",
428                last_error()
429            );
430            assert_eq!(pamoja_i2c_bus_waited_micros(bus), 500);
431
432            let held = pamoja_i2c_bus_part(bus, 0x40);
433            assert_eq!(
434                pamoja_i2c_part_register(held, pca9685::register::PRE_SCALE),
435                121
436            );
437            assert_eq!(
438                pamoja_i2c_part_register(held, pca9685::register::MODE1),
439                0x20
440            );
441            let loaded = PamojaPwm {
442                on_low: pamoja_i2c_part_register(held, 0x06),
443                on_high: pamoja_i2c_part_register(held, 0x07),
444                off_low: pamoja_i2c_part_register(held, 0x08),
445                off_high: pamoja_i2c_part_register(held, 0x09),
446            };
447            assert_eq!(loaded, servo);
448            pamoja_i2c_part_free(held);
449
450            let mut read_back = actuators::pamoja_pwm_full_on();
451            assert_eq!(
452                pamoja_pca9685_channel(driver, 0, &mut read_back),
453                PamojaStatus::Ok
454            );
455            assert_eq!(read_back, servo, "the driver reads the channel back");
456            assert_eq!(
457                pamoja_pca9685_channel(driver, 16, &mut read_back),
458                PamojaStatus::InvalidArgument
459            );
460            assert_eq!(
461                pamoja_pca9685_channel(driver, 0, ptr::null_mut()),
462                PamojaStatus::InvalidArgument
463            );
464
465            assert_eq!(
466                pamoja_pca9685_set_all(driver, actuators::pamoja_pwm_full_off()),
467                PamojaStatus::Ok
468            );
469            let held = pamoja_i2c_bus_part(bus, 0x40);
470            assert_eq!(
471                pamoja_i2c_part_register(held, 0x09),
472                0x10,
473                "channel 0 off again"
474            );
475            pamoja_i2c_part_free(held);
476
477            assert_eq!(pamoja_pca9685_software_reset(driver), PamojaStatus::Io);
478            assert!(last_error().contains("0x00"), "{}", last_error());
479
480            pamoja_pca9685_free(driver);
481            pamoja_i2c_bus_free(bus);
482        }
483    }
484
485    #[test]
486    fn a_null_driver_is_refused() {
487        unsafe {
488            assert_eq!(
489                pamoja_pca9685_init(ptr::null_mut()),
490                PamojaStatus::InvalidArgument
491            );
492            assert_eq!(pamoja_pca9685_prescale(ptr::null()), 0);
493            let mut driver = ptr::null_mut();
494            assert_eq!(
495                pamoja_pca9685_new(
496                    ptr::null(),
497                    0x40,
498                    pamoja_pca9685_settings_default(),
499                    &mut driver
500                ),
501                PamojaStatus::InvalidArgument
502            );
503            assert!(driver.is_null());
504        }
505    }
506}