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.
Diff: AD9850SPI.cpp
- 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