banging

STM32 programming – Bit or Byte ‘Banging’ on AD9850

Bit or Byte banging, what is that exactly?!

Sometimes device(s) has no standard serial or parallel protocols as are I2C, SPI and others. Instead, just series of bits or bytes and separate clock. Excellent example is DDS chip AD9850 – we may chose or serial or parallel interfacing. On serial interfacing we using ‘bit banging’ – we put one bit on GPIO port, then on another GPIO port one clock, then next bit on port… until all bites are transferred. Then we send frequency and phase update on third wire, brief pulse 1 then 0. If we want parallel (8 times faster) interfacing, we put whole byte on 8 pins, in our example GPIO A0 to A7, then on another wire we ‘fire’ clock pulse, and after transferring all data, also ‘firing’ update frequency and phase.  Before proceed to our program in Keil, we should to consult our datasheet about AD9850:

PDF Document

FM radio or Frequency Wobbler

In the codes below, there are two options: FM radio (chose for example 1/3 of the frequency, say 33 MHz, then ‘catch’ third harmonic on 99 MHz, and use comparator for getting almost square wave, so that odd harmonics are stronger), or “Frequency Wobbler”, excellent tool for RLC circuit characterization (testing band pass, low pass, notch and other filters as well as resonant circuits) from 0 to 62.5 MHz with AD9850 – or up to 90 MHz with AD9851, which is really hard to find.  You may change code so that sweep is only upward, and then program one pin before sweep to output short pulse for oscilloscope external trigger. Or leave as is and listen on SDR Software Defined Radio response from RLC circuits.

I made both options available in the same program, just pay attention to GPIOA registers – there are difference in the case of serial and in the case of parallel interfacing. Also, for serial or parallel, there are functions “dds_update_freqS” and “dds_update_freqP”, probably you correctly guessed, ‘P’ is for parallel, ‘S’ is for serial. Now, copy paste this code as usually onto your Keil editor:

		/********************************************************************** 
		 Short but sweet sweep program. It goes from wanted low and high 
		 frequency with desired steps. In this example with parallel mode,
		 it goes 8 times faster than in serial mode. 
		 
		======================Pin map:=======================
		STM32F103C8      AD9850       ST-LINK V2
		GND---------------GND-----------GND
		VCC----------------------------+3.3V
		..................VCC----------+5V
		B1----------------WCLK
		B10---------------FQUP
		B11---------------RESET
		A0----------------D0
		A1----------------D1
		A2----------------D2
		A3----------------D3
		A4----------------D4
		A5----------------D5
		A6----------------D6
		A7----------------D7
		======================================================
		This example is free to use, share modify... do what you want. :D
		*******************************************************************/

#include "stm32f10x.h"

uint8_t phase = 0;
int    analog = 0;
//in the case of sweep function:
int sweep_begin = 0;
int sweep_end   = 0;
int sweep_step  = 0;
//in the case of FM radio:
float freq= 26e6;    // "Intended" frequency (33e6= 33 000 000 Hz, or 33 MHz), but:
float	offset= 49.5e3; // Since ADC is directly connected to analog input, it has some non-zero voltage. 
	               // This voltage should be ideally VCC/2, or 3.3V/2= 1.65V, but it is not always true.
	               // So, offset should be subtracted from intended frequency so that final frequency is where we want.
	               // Set this number zero, then look at your receiver (RTL SDR, for example), how much it goes up, 
	               // then set this offset. This number is subtracted inside update (in IRQ handler function).
	      			   // In my case, it is 49.0 KHz above, so positive value will be calculated inside IRQ thing (freq-offset)

void delay(long cycles)
{
  while(cycles >0)
  cycles--; // Some stupid delay, it is not in milliseconds or microseconds, but rather in some 'wasted clock cycles'
}

void ADC_enable(void)
{
	RCC->CFGR |= RCC_CFGR_ADCPRE_DIV6;// ADC clock = 12 MHz, maximum is 14, but there is no divider for that freq (72MHz / 6 = 12MHz). 
	RCC->APB2ENR |= RCC_APB2ENR_ADC1EN | RCC_APB2ENR_AFIOEN; //enabling ADC clock, interrupt enable, 
	//while clock for port A and B is enabled down below

	ADC1->CR1 |=ADC_CR1_EOCIE;   //ADC interrupt enabled
	NVIC_EnableIRQ(ADC1_2_IRQn); //interrupt enabled
	
	ADC1->SMPR2 |= ADC_SMPR2_SMP8_0|ADC_SMPR2_SMP8_1|ADC_SMPR2_SMP8_2;
	ADC1->SQR3 |= ADC_SQR3_SQ1_3; //for B0 in sequence 1, channel 8, it is 0b1000 = 8 (IN8)
	//ADC1->SQR3 |=8; // alternative way of setting the same thing as above

	ADC1->CR2 &= ~ADC_CR2_ALIGN; //data is right aligned (0bxxxx111111111111)

	ADC1->CR2 |= ADC_CR2_ADON | ADC_CR2_CONT; //ADC converter is on
	delay(1000); //alow ADC to stabilize - 1 mS, but my delay is not exactly 1 mS, it is much shorter...
    ADC1->CR2 |= ADC_CR2_CAL;
	delay(1000); //it is better to leave some time, just few clock cycles...
	ADC1->CR2 |= ADC_CR2_ADON; //not sure it requires to call it again?
	delay(1000); //After first ADON, ADC is just set, then second time ADC is actually enabled
}

void gpio_A_and_B_port_enable(void)
{
	RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN; //ports A & B clock enabled
	/* For parallel connection: */
	//GPIOA->CRL = 0x33333333; //for parallel - pins A0 to A7 are used, 50 MHz, push-pull
	/* For serial connection: */
	GPIOA->CRL = 0x34444444; //for serial, only pin A7 is used, other pins MUST be floating
	GPIOB->CRL = 0x44444430; //Pin B0 is analog input, pin B1 is digital output, 50 MHz, push-pull
	GPIOB->CRH = 0x44443344; //Pins B10 and B11 are digital putput, 50 MHz, push-pull
}

void reset_d(void)
{
	GPIOB->BSRR = GPIO_BSRR_BS11; // Reset, pin B11
  GPIOB->BSRR = GPIO_BSRR_BR11;
}

void clock_d(void)
{
	GPIOB->BSRR = GPIO_BSRR_BS1;  // Clock, pin B1
	GPIOB->BSRR = GPIO_BSRR_BR1;
}

void update_d(void)
{
	GPIOB->BSRR = GPIO_BSRR_BS10; // Update, pin B10
	GPIOB->BSRR = GPIO_BSRR_BR10;
}


void dds_reset(void)
{
	// Set everything low first
	GPIOB->BRR = GPIO_BRR_BR1|GPIO_BRR_BR10|GPIO_BRR_BR11; //instead BSRR, we use BRR, Bit Reset Retister
	reset_d();  // Pulse reset
	clock_d();  // Pulse clock
	update_d(); // Pulse load
}

void dds_writeParallel(uint8_t byte)
{
	GPIOA->ODR = byte;
	clock_d();
}

void dds_writeSerial(uint8_t byte)
{
  uint8_t i;
  uint8_t bit;
  for(i = 0; i < 8; i++) 
	{
      bit = ((byte >> i) & 1);
      if(bit == 1)
         GPIOA->BSRR = GPIO_BSRR_BS7; // Data bit set on pin A7
      else
         GPIOA->BSRR = GPIO_BSRR_BR7; //data bit reset (A7)
         clock_d();
  }
}

/* Parallel updating frequency - different than serial */
void dds_update_freqP(float freq)
{
    uint32_t tuning_word = (freq * 34.36426); // resolution is 0.0291 Hz, so 1/0.0291 = 34.36426 steps per 1 Hz
	/* Another chip, AD9851 has almost the same properties. Just different resolution + ability to use low frequency
	   oscillator by enabling internal multiplier x6 on first word W0, which should be: 0b00000010 */
	dds_writeParallel(((uint8_t)0b00000000) & 0xFF); // 0b00000100 = W0* - power off - should be phase + pwr_down + control bits
	//0bxxxxx, five upper bits for phase, 180/90/45/22.5/11.25 degrees, then one bit for power down (if set), and two zeroes (reserved)
	dds_writeParallel((tuning_word >> 24) & 0xFF);
	dds_writeParallel((tuning_word >> 16) & 0xFF);
	dds_writeParallel((tuning_word >> 8) & 0xFF);
	dds_writeParallel(tuning_word & 0xFF);
	update_d();// Load (f_upd)
	// *-> in parallel mode, it is possible to send only W0 for changing phase and power up/down.
}

/* Serial updating frequency - different than in parallel */
void dds_update_freqS(float freq)
{
	// Updates DDS output frequency. Supply frequency in Hz.
	uint32_t tuning_word = (freq * 34.36426); // resolution is 0.0291 Hz, so 1/0.0291 = 34.36426 steps per 1 Hz
	dds_writeSerial(tuning_word & 0xFF);
	dds_writeSerial((tuning_word >> 8) & 0xFF);
	dds_writeSerial((tuning_word >> 16) & 0xFF);
	dds_writeSerial((tuning_word >> 24) & 0xFF);
	dds_writeSerial(0b00000000); // 0b00000100 = power off
	update_d(); // this function finally send signal to AD9850 to update frequency and phase
}

void sweep(int sweep_begin, int sweep_end, int sweep_step)
{
	for (int i=sweep_begin;i<sweep_end;i+=sweep_step)
		{
			dds_update_freqS(i);
			//delay(50); //if you want slow up upward sweeping
		}
		//now going back. If you want only one way (up), then remove or comment (/**/) for loop below for only down,
		//do the same with for loop above. 
		for (int i=sweep_end;i>sweep_begin;i-=sweep_step)
		{
			dds_update_freqS(i);
			//delay(50); //if you want to slow down sweeping downward
		} 
}

void ADC1_2_IRQHandler(void)
{
	  if (ADC1->SR & ADC_SR_EOC) 
	{
		analog=ADC1->DR;
		dds_update_freqS((freq-offset)+(analog*26)); //FM modulation. No multiplier for NFM, but for WFM it is an*26 or so...
	}
} 

int main()
{
	gpio_A_and_B_port_enable();	
	dds_reset(); 
	ADC_enable();

  while (1) 
  {
	  //sweep(25e6,26e6,1e3); //frequency begin, frequency end, step size
  }
}

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.