AFSK modulation experiments - success!
So, in the previous blog entry, I confirmed that I had come up with some numbers and timer values to make the frequencies I needed. For posterity, I'll document them here.
DAC configration
I'm running my DAC with 24 data points. I generated my sinewave table using the script that I blogged previously. Based on the results from AFSK modulation experiments - part 4, I set the PPM on the Si5351b to 30, and the DAC_PCT to 35 to achieve about 3000hz deviation. It looks as follows:#define DAC_PCT 35 // Percentage of DAC output#define SINE_RES 24const uint16_t sinewave[SINE_RES] = { (0 * DAC_PCT / 100) + 2048, (530 * DAC_PCT / 100) + 2048, (1023 * DAC_PCT / 100) + 2048, (1448 * DAC_PCT / 100) + 2048, (1773 * DAC_PCT / 100) + 2048, (1978 * DAC_PCT / 100) + 2048, (2047 * DAC_PCT / 100) + 2048, (1978 * DAC_PCT / 100) + 2048, (1773 * DAC_PCT / 100) + 2048, (1448 * DAC_PCT / 100) + 2048, (1024 * DAC_PCT / 100) + 2048, (530 * DAC_PCT / 100) + 2048, (0 * DAC_PCT / 100) + 2048, (-530 * DAC_PCT / 100) + 2048, (-1023 * DAC_PCT / 100) + 2048, (-1448 * DAC_PCT / 100) + 2048, (-1773 * DAC_PCT / 100) + 2048, (-1978 * DAC_PCT / 100) + 2048, (-2047 * DAC_PCT / 100) + 2048, (-1978 * DAC_PCT / 100) + 2048, (-1773 * DAC_PCT / 100) + 2048, (-1448 * DAC_PCT / 100) + 2048, (-1024 * DAC_PCT / 100) + 2048, (-530 * DAC_PCT / 100) + 2048,};
Clock speed
I'm testing on the Nucleo-L152RE, which has a STM32L152RET6 processor. My flght board uses an STM32L152CB, which is quite similar. I'm running it at a clock speed of 32 MHZ, and the prescalars set such that the APB1 clock speed is also 32mhz.
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI; RCC_OscInitStruct.HSIState = RCC_HSI_ON; RCC_OscInitStruct.HSICalibrationValue = 16; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI; RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL4; // 32mhz RCC_OscInitStruct.PLL.PLLDIV = RCC_PLL_DIV2; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
Timer calculations
Based on the 32mhz clock, I calculated timers as follows. The goal for the DAC timer was to be able to simply toggle the "counter" value back and forth, in order to change the tone between 1200hz and 2200hz. You can calculate the Clock Ticks yourself by dividing:
Clock_Ticks_Hz = 32000000 / (DAC Datapoints * Prescaler * Counter)
Function | Timer | DAC sinewave data points | Prescaler | Counter | Clock ticks (hz) |
---|---|---|---|---|---|
Baud Timer 1200hz | TIM2 | n/a | 13,333 | 2 | 1200.03001 |
DAC timer 1200 hz | TIM6 | 24 | 101 | 11 | 1200.120012 |
DAC timer 2200 hz | TIM6 | 24 | 101 | 6 | 2200.220022 |
htim2.Instance = TIM2;
htim2.Init.Prescaler = 13333 - 1;
htim2.Init.Period = 2 - 1; // 1200 hz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
if (HAL_TIM_Base_Init(&htim2) != HAL_OK)
{
Error_Handler(__FILE__, __LINE__);
}
htim6.Instance = TIM6;
htim6.Init.Prescaler = 101 - 1;
htim6.Init.Period = 11 - 1;
// 1200 hz
htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
if (HAL_TIM_Base_Init(&htim6) != HAL_OK)
{
Error_Handler(__FILE__, __LINE__);
}
// We turn on the ARPE register so that the counter is only updated AFTER a clean
// count completes. Otherwise, we run the risk of overcounting an interval.
htim6.Instance->CR1 |= TIM_CR1_ARPE; // Turn on auto-preload register
The ARPE bit
Note that on the DAC timer (TIM6) we set the ARPE bit in the Control Register. This causes any changes to the counter to take effect AFTER the next timer pop. This assures that each timer cycle is complete, so that the DAC data points are properly spaced.
Switching between 1200hz and 2200hz
Because I'm using the DAC to make the sine wave, and just adjusting the TIM6 timer to change the frequency, I am able to greatly simplify the code that most APRS implementations use. Rather than using a 512 element phase array, I'm simply using a 24 element sine array for the DAC. I no longer need to track the phase position when switching. Rather, I just adjust the timer counter.The code is simple, but it confused me at first, because AX.25 is using NRZI encoding. So, here's a bit about that, before showing the code.
From Wikipedia:
At the datalink level, AX.25 specifies HDLC (ISO 3309)[3] frames transmitted with NRZI encoding.In NRZI encoding, you're not sending the actual bits, but rather toggling whatever bit you are sending, based on the next data value. I borrowed a few lines from the Tracksoar code, and modified them to my purposes:
if ((current_byte & 1) == 0) {
// Toggle tone (1200 <> 2200)
current_timer_counter ^= (COUNTER_1200 ^ COUNTER_2200); // Switch to the opposite counter value
__HAL_TIM_SET_AUTORELOAD(&htim6,current_timer_counter); // Set frequency
}
That's it. No math about phase arrays and accmulators! Yay!
Initial test failures
I only had 3 tiny bugs in my code which prevented APRS from working.
- I actually FORGOT the HAL_TIM_SET_AUTORELOAD command after setting the current_timer_counter. lol.
- The more subtle one was that I had misread the manual, and thought that the ARPE bit was set by default. When I tested without it, there were hesitations in the middle of the transmission. It was obvious what was going on when I heard it, and simple to fix.
- I contrived the test by hard-coding GPS values into the strings that the aprssend() call uses. I used the wrong number of digits in the Longitude, and so my Kenwood D72 caught my callsign, it wouldn't decode it until I fixed that coordinate. Along the way, I noticed that my APRS comment was also too long. I wanted it to read "Mr. Watson, come here, I want you." Unfortunately, that's too many characters. So, I changed it to "Mr. Watson, Come here!..."