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
- Branch:
- switch-to-cmsis-bitops
- Revision:
- 4:c60c1caa2593
- Parent:
- 1:567d552aa1a4
- Child:
- 6:8c15ffb18f88
--- a/AD9850SPI.cpp Thu Jan 09 17:30:19 2020 +0000 +++ b/AD9850SPI.cpp Fri Jan 10 10:24:30 2020 +0900 @@ -1,170 +1,143 @@ -/** - * 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 == _BIG_ENDIAN - *(unsigned int *)buffer = htole32(scaled_freq); -#else - *(unsigned int *)buffer = htobe32(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); +/** + * 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 + +/** + * 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 = __RBIT(freq * (UINT32_MAX / AD9850_CLOCK_REF)); + unsigned int scaled_phase = __RBIT(phase * (UINT5_MAX / AD9850_PHASE_MAX)); + + char buffer[5]; + *(unsigned int *)buffer = __REV(scaled_freq); + 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