Fast SPI-based serial interface to AD9850 clock generator. Has same interface as (bit-banging based) AD9850 library, but more than x100 faster in frequency update speed. Can sweep 1KHz to 30MHz in 1KHz step in a few seconds.

Revision:
0:4aaced3b219c
Child:
1:567d552aa1a4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/AD9850SPI.cpp	Thu Jan 09 16:48:45 2020 +0000
@@ -0,0 +1,170 @@
+/**
+ * AD9850 driver using hardware SPI.
+ * 
+ * - http://www.analog.com/static/imported-files/data_sheets/AD9850.pdf
+ * - http://developer.mbed.org/users/liamg/code/AD9850-function-generator-SPI-driver/
+ */
+
+#include "AD9850SPI.h"
+
+#ifndef AD9850_CLOCK_REF
+#define AD9850_CLOCK_REF 125000000U
+#endif
+
+#ifndef UINT32_MAX
+#define UINT32_MAX -1U
+#endif
+
+#define UINT5_MAX 31
+#define AD9850_PHASE_MAX 360
+
+#if _BYTE_ORDER == _BIG_ENDIAN
+#define htole32(v) __bswap32(v)
+#define htobe32(v) (v)
+#else
+#define htole32(v) (v)
+#define htobe32(v) __bswap32(v)
+#endif
+
+/**
+ * Reverse bit order (not endian byte order, but bit order of whole 32bits).
+ * See:
+ * - http://aggregate.org/MAGIC/#Bit%20Reversal
+ * - https://github.com/hcs0/Hackers-Delight/blob/master/reverse.c.txt
+ */
+static inline unsigned int
+reverse(unsigned int x) {
+    x = (((x & 0xaaaaaaaa) >> 1) | ((x & 0x55555555) << 1));
+    x = (((x & 0xcccccccc) >> 2) | ((x & 0x33333333) << 2));
+    x = (((x & 0xf0f0f0f0) >> 4) | ((x & 0x0f0f0f0f) << 4));
+    x = (((x & 0xff00ff00) >> 8) | ((x & 0x00ff00ff) << 8));
+    return ((x >> 16) | (x << 16));
+}
+
+/**
+ * Create AD9850SPI instance.
+ *
+ * SPI instance should be externally created and passed in,
+ * with DATA as MOSI and W_CLK as SCLK. Note that NC can be
+ * given for MISO, as AD9850 does not have any output.
+ *
+ * @param spi SPI interface to use
+ * @param fu_ud Chip select pin
+ * @param reset Reset pin
+ */
+AD9850SPI::AD9850SPI(SPI &spi, PinName fq_ud, PinName reset_pin)
+    : _spi(spi), _fq_ud(fq_ud), _reset(reset_pin) {
+    reset();
+}
+
+AD9850SPI::~AD9850SPI() {}
+
+/**
+ * Reset SPI parameter (only) to match with AD9850 requirement.
+ *
+ * This is useful when sharing SPI bus line with multiple chips
+ * with different SPI parameter.
+ */
+void
+AD9850SPI::reset_spi(void) {
+    //
+    // Configure SPI mode. AD9850 isn't actually an SPI, but can be
+    // handled as SPI in following manner:
+    //
+    // - SCLK is low on inactive (CPOL=0)
+    // - DATA is sampled on asserting edge (CPHA=0)
+    // - SSEL needs to be kept low during whole 40-bit transfer
+    //   - Hardware controlled SSEL tends to only support up to 16-bit transfer
+    // 
+    // So AD9850 can be handled as SPI with CPOL=0, CPHA=0, with some
+    // nonstandard pins that needs to be controlled separately.
+    // 
+    // For SSEL, FQ_UD pin can be specified as GPIO-based SSEL in SPI module.
+    // However, it is separately controlled in this code as semantic of FQ_UD
+    // pin is quite different from usual SSEL. FQ_UD is expected to be usually
+    // low, and expected to go high-low to finalize loaded value.
+    //
+
+    _spi.format(8 /* bits */, 0 /* mode */);
+}
+
+/**
+ * Reset device and enable serial mode.
+ * Also reconfigures SPI bus parameter for transfer.
+ */
+void
+AD9850SPI::reset(void) {
+    // initialize bus parameter first
+    reset_spi();
+
+    //
+    // DS Figure 7. Master Reset Timing Sequence
+    //
+    // - Minimum reset width is 5 CLKIN cycles
+    // - Minimum reset latency is 13 CLKIN cycles
+    // - Minimum reset delay from clkin edge is 3.5ns (= ~142MHz)
+    //
+    // Slowest reference clock supported by AD9850 is 1MHz (1us cycle time).
+    // Even in that case, ~20us would be enough for resetting.
+    //
+
+    _reset = 0; wait_us(20); // just in case
+    _reset = 1; wait_us(6);
+    _reset = 0; wait_us(14);
+
+    //
+    // DS Figure 10. Serial Load Enable Sequence
+    //
+    // - Set hardware pins to D0=1, D1=1, D2=0
+    // - Send a single pulse in W_CLK, then to FU_UD
+    //   - This is actually just a parallel load of D[012] values
+    //
+    // A dummy write to SPI followed by FU_UD update should do the trick.
+    //
+
+    _fq_ud = 0; wait_us(1);
+    _spi.write(0);
+    _fq_ud = 1; wait_us(1);
+    _fq_ud = 0; wait_us(1);
+}
+
+/**
+ * Set frequency.
+ * Note frequency should be <33% of reference clock when used as a clock generator.
+ *
+ * @param freq      Frequency in Hz (maps to W0-W31)
+ * @param powerdown Power-down bit (maps to W34)
+ * @param phase     Phase in degrees (maps to W35-W39)
+ */
+void
+AD9850SPI::setFrequency(int freq, int powerdown, int phase) {
+    //
+    // DS Table IV. 40-Bit Serial Load Word Function Assignment
+    //
+    // - 40bit data in total
+    // - W0-W31: Frequency normalized to 32-bit value (LSB-first)
+    // - W32-W33: MUST be 0 (DS Table II. Factory Reserved Internal Test Codes)
+    // - W34: Power-down bit
+    // - W35-W39: Phase normalized to 5-bit scaled value (LSB-first)
+    //
+    // AD9850 requires LSBit-first transfer while SPI is MSBit-first.
+    // So bit order needs to be reversed before passing the data to SPI.
+    //
+
+    unsigned int scaled_freq  = reverse(freq * (UINT32_MAX / AD9850_CLOCK_REF));
+    unsigned int scaled_phase = reverse(phase * (UINT5_MAX / AD9850_PHASE_MAX));
+
+    char buffer[5];
+#if _BYTE_ORDER == _LITTLE_ENDIAN
+    *(unsigned int *)buffer = htobe32(scaled_freq);
+#else
+    *(unsigned int *)buffer = htole32(scaled_freq);
+#endif
+    buffer[4] = 0b00111111 & (
+        ((!!powerdown) << 5) | (scaled_phase >> 27)
+    );
+
+    _spi.write(buffer, sizeof(buffer), NULL, 0);
+    _fq_ud = 1; wait_us(1);
+    _fq_ud = 0; wait_us(1);
+}
\ No newline at end of file