1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
#![no_std]
//! # `ad9850` - Embedded driver for the AD9850 DDS synthesizer chip
//!
//! The AD9850 is a DDS Synthesizer chip sold by Analog Devices. Check the [datasheet](https://www.analog.com/media/en/technical-documentation/data-sheets/AD9850.pdf) for general information about it.
//!
//! This crate implements an interface for embedded devices to control such an AD9850 chip.
//!
//! The only dependency of this crate is that your device has 4 digital output pins which implement the [`embedded_hal::digital::v2::OutputPin`] trait.
//!
//! ## Usage example
//!
//! This example uses the [`arduino-hal`](https://github.com/Rahix/avr-hal). The `ad9850` library is not device specific though, so
//! it should be easy to adapt the example to other devices.
//!
//! ```ignore
//! #[arduino_hal::entry]
//! fn main() -> ! {
//!     let dp = arduino_hal::Peripherals::take().unwrap();
//!     let pins = arduino_hal::pins!(dp);
//!
//!     // Initialize the device
//!     let ad9850 = ad9850::Ad9850::new(
//!         pins.d4.into_output(), // Connect D4 (Arduino) to RESET (AD9850)
//!         pins.d5.into_output(), // Connect D5 (Arduino) to DATA (AD9850)
//!         pins.d6.into_output(), // Connect D6 (Arduino) to FQ_UD (AD9850)
//!         pins.d7.into_output(), // Connect D7 (Arduino) to W_CLK (AD9850)
//!     ).into_serial_mode().unwrap();
//!     //                   ^^^^ unwrap is ok here, since `set_low`/`set_high`
//!     //                        are infallible in the arduino-hal.
//!
//!     // Set output frequency to 1 MHz
//!     ad9850.set_frequency(1000000.0);
//! }
//! ```
//!
//! ## Supported features
//!
//! - [x] Reset the device
//! - [x] Program in Serial mode
//! - [ ] Program in Parallel mode
//! - [ ] Power down / wakeup
//!
//! ## A note about timing
//!
//! Communication with the Ad9850 involves sending "pulses" on the
//! RESET, W_CLK and FQ_UD lines. According to the datasheet, these
//! pulses must be at least $3.5ns$ long for RESET and W_CLK and at
//! least $7ns$ for the FQ_UD line.
//!
//! This implementation does not insert any "delay" between toggling
//! the pins high and low, so the pulse width depends on the CPU frequency
//! of the device this code is run on.
//!
//! Example: if the MCU runs at 16 MHz, the minimum pulse width attained
//! this way is $\frac{1}{16MHz} = 62ns$, which is way above the
//! required width of $7ns$.
//!
//! If your MCU runs at a significantly higher frequency, this approach
//! may fail and you'll have to modify the code to insert delays.
//!
//! Feel free to [open an issue](https://github.com/nilclass/ad9850-rs/issues/new), if you run into timing issues.

use core::marker::PhantomData;
use embedded_hal::digital::v2::OutputPin;

/// Default frequency of the oscillator connected to the AD9850.
pub const DEFAULT_OSCILLATOR_FREQUENCY: f32 = 125e6;

/// Represents a connection to a AD9850 device.
///
/// See [crate level documentation](crate), or check the [`new`](Ad9850::new) method for an entry point.
pub struct Ad9850<Mode, Reset, Data, FqUd, WClk> {
    osc_freq: f32,
    reset: Reset,
    data: Data,
    fq_ud: FqUd,
    w_clk: WClk,
    marker: PhantomData<Mode>,
}

impl<Reset, Data, FqUd, WClk, E> Ad9850<mode::Init, Reset, Data, FqUd, WClk>
where
    Reset: OutputPin<Error = E>,
    Data: OutputPin<Error = E>,
    FqUd: OutputPin<Error = E>,
    WClk: OutputPin<Error = E>,
{
    /// Construct a new Ad9850 instance, in inital mode.
    ///
    /// This call does not communicate with the device yet. You need to call `into_serial_mode` to initiate a reset before you can send any data.
    ///
    /// Example:
    /// ```ignore
    /// let ad9850 = Ad9850::new(
    ///     pins.d4.into_output(),
    ///     pins.d5.into_output(),
    ///     pins.d6.into_output(),
    ///     pins.d7.into_output(),
    /// ).into_serial_mode().unwrap();
    /// ```
    ///
    /// The four parameters correspond to the digital pins connected to the AD9850:
    ///
    /// | Signal | AD9850 Pin |
    /// |--------|------------|
    /// | reset  |     22     |
    /// | data   |     25     |
    /// | fq_ud  |      7     |
    /// | w_clk  |      8     |
    ///
    /// NOTE: the "data" pin is labeled "D7" in the AD9850 datasheet. It's used either as the MSB in parallel mode, or as the single data pin in serial mode.
    ///   Breakout boards usually expose this pin twice: once as D7 on the parallel interface, and again as a pin labeled DATA. For this reason, make sure
    ///   not to ground the D7 pin if you are planning to use serial mode.
    ///
    pub fn new(reset: Reset, data: Data, fq_ud: FqUd, w_clk: WClk) -> Self {
        Self {
            reset,
            data,
            fq_ud,
            w_clk,
            osc_freq: DEFAULT_OSCILLATOR_FREQUENCY,
            marker: PhantomData,
        }
    }

    /// Same as [`new`](Ad9850::new), but allows the oscillator frequency to be specified.
    ///
    /// Use this if your board's oscillator is **not** 125 MHz.
    ///
    /// This value is used in calculations done by [`set_frequency`](Ad9850::set_frequency) and [`set_frequency_and_phase`](Ad9850::set_frequency_and_phase).
    pub fn new_with_osc_freq(
        reset: Reset,
        data: Data,
        fq_ud: FqUd,
        w_clk: WClk,
        osc_freq: f32,
    ) -> Self {
        Self {
            reset,
            data,
            fq_ud,
            w_clk,
            osc_freq,
            marker: PhantomData,
        }
    }

    /// Reset the ad9850 device into serial mode.
    ///
    /// This is the only supported mode at the moment.
    ///
    /// Returns an error, if any of the `set_low` / `set_high` calls on one of the pins fail.
    pub fn into_serial_mode(mut self) -> Result<Ad9850<mode::Serial, Reset, Data, FqUd, WClk>, E> {
        // RESET pulse resets the registers & mode to default
        self.reset.set_high()?;
        self.reset.set_low()?;

        // single W_CLK pulse followed by FQ_UD pulse switches to serial input mode
        self.w_clk.set_high()?;
        self.w_clk.set_low()?;
        self.fq_ud.set_high()?;
        self.fq_ud.set_low()?;

        Ok(Ad9850 {
            reset: self.reset,
            data: self.data,
            fq_ud: self.fq_ud,
            w_clk: self.w_clk,
            osc_freq: self.osc_freq,
            marker: PhantomData,
        })
    }
}

/// Utility functions
pub mod util {
    pub fn frequency_to_tuning_word(output_frequency: f32, oscillator_freq: f32) -> u32 {
        (output_frequency / (oscillator_freq / 4294967296.0)) as u32
    }

    /// Turns phase into AD9850 register format.
    ///
    /// This function turns the given `phase` (a 5-bit unsigned integer) into
    /// a value suitable to be passed to [`crate::Ad9850::update`] as a control
    /// byte.
    ///
    /// If the given `phase` overflows 5 bits, `None` is returned.
    ///
    /// Example:
    /// ```
    /// # use ad98509::util::phase_to_control_byte;
    /// assert_eq!(Some(0b00001000), phase_to_control_byte(1));
    /// assert_eq!(Some(0b10000000), phase_to_control_byte(1 << 4));
    /// assert_eq!(None, phase_to_control_byte(1 << 5));
    /// ```
    pub fn phase_to_control_byte(phase: u8) -> Option<u8> {
        if phase > 0x1F {
            None
        } else {
            Some(phase << 3 & 0xF8)
        }
    }
}

impl<Reset, Data, FqUd, WClk, E> Ad9850<mode::Serial, Reset, Data, FqUd, WClk>
where
    Reset: OutputPin<Error = E>,
    Data: OutputPin<Error = E>,
    FqUd: OutputPin<Error = E>,
    WClk: OutputPin<Error = E>,
{
    /// Set output frequency to the given value (in Hz)
    ///
    /// Convenience function to set only the frequency, with a phase of $0$.
    /// Computes the proper tuning word (assuming an oscillator frequency of 125MHz)
    /// and calls `update`.
    pub fn set_frequency(&mut self, frequency: f32) -> Result<(), E> {
        self.update(util::frequency_to_tuning_word(frequency, self.osc_freq), 0)
    }

    /// Set output frequency and phase to given values
    ///
    ///
    ///
    ///
    /// # Panics
    ///
    /// The given `phase` must not exceed 5 bits (decimal 31, 0x1F). Otherwise this function panics.
    ///
    pub fn set_frequency_and_phase(&mut self, frequency: f32, phase: u8) -> Result<(), E> {
        self.update(
            util::frequency_to_tuning_word(frequency, self.osc_freq),
            util::phase_to_control_byte(phase).expect("invalid phase"),
        )
    }

    /// Update device register with given data.
    ///
    /// This is a low-level interface. See the `set_*` methods for a high-level wrapper.
    ///
    /// The Ad9850 register is 40-bit wide:
    /// - The first 32 bit are the "tuning word", which determines the frequency
    /// - Next two bits are "control bits", which should always be zero (enforced by this function)
    /// - This is followed by a single "power down" bit
    /// - The remaining 5 bits determine the "phase" by which the output signal is shifted.
    ///
    /// To find the correct tuning word for a given frequency that you want to set, you'll need
    /// to know the frequency of the oscillator connected to the Ad9850.
    /// Assuming an oscillator frequency of 125 MHz (the default on most breakout boards),
    /// then the tuning word $tw$ for a given frequency $f$ can be found with this formula:
    ///
    /// $ tw = \frac{f}{2^{32} * 125MHz} $
    pub fn update(&mut self, tuning_word: u32, control_and_phase: u8) -> Result<(), E> {
        self.shift_out((tuning_word & 0xFF) as u8)?;
        self.shift_out((tuning_word >> 8 & 0xFF) as u8)?;
        self.shift_out((tuning_word >> 16 & 0xFF) as u8)?;
        self.shift_out((tuning_word >> 24 & 0xFF) as u8)?;
        // force control bits to be zero, to avoid surprises
        self.shift_out(control_and_phase & 0b11111100)?;
        self.fq_ud.set_high()?;
        self.fq_ud.set_low()?;
        Ok(())
    }

    fn shift_out(&mut self, byte: u8) -> Result<(), E> {
        // shift out to DATA, at rising edge of W_CLK (LSB first)
        for i in 0..8 {
            self.data.set_state(((byte >> i) & 1 == 1).into())?;
            self.w_clk.set_high()?;
            self.w_clk.set_low()?;
        }
        Ok(())
    }
}

/// Marker types for different modes.
///
/// These types are used for the `Mode` type parameter of [`Ad9850`].
pub mod mode {
    /// Initial mode. No communication has happened.
    pub struct Init;
    /// Serial mode. Device is reset and put into the proper mode. Updates can happen.
    pub struct Serial;
}