diff --git a/README.md b/README.md index 722399e..95b008d 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,28 @@ -This firmware template is designed for development boards based around [STM32 F4 series micro-controller](https://www.st.com/en/microcontrollers-microprocessors/stm32f4-series.html). +clock generator (up to 125 MHz) using AD9850. -project -======= +capabilities: -summary -------- +- up to 125 MHz@5V, 100MHz@3.3V +- display shows desired frequency in mHz +- actual output uses AD9850's 0.0291 Hz resolution +- square and sine wave output (and it's inverse) +- 3.3V and 5V output -*describe project purpose* +hardware +======== -technology ----------- - -*described electronic details* - -board -===== - -The underlying template also supports following board: +the clock generator uses following parts: - [WeAct MiniF4](https://github.com/WeActTC/MiniF4-STM32F4x1), based on a STM32F401CCU6 - -**Which board is used is defined in the Makefile**. -This is required to map the user LED and button provided on the board +- Analog Devices AD9850, to generator the clock +- HD44780 LCD display (1x16), to display the set frequency +- rotary encoder, to set the frequency +- switch, to set the output voltage +- coin cell, to save set frequency connections =========== -Connect the peripherals the following way (STM32F4xx signal; STM32F4xx pin; peripheral pin; peripheral signal; comment): - -- *list board to peripheral pin connections* - All pins are configured using `define`s in the corresponding source code. code diff --git a/application.c b/application.c index ba21734..06ff473 100644 --- a/application.c +++ b/application.c @@ -1,8 +1,8 @@ -/** STM32F4 application example +/** clock generator, using AD9850 * @file * @author King Kévin * @copyright SPDX-License-Identifier: GPL-3.0-or-later - * @date 2016-2021 + * @date 2016-2022 */ /* standard libraries */ @@ -11,6 +11,7 @@ #include // string utilities #include // date/time utilities #include // utilities to check chars +#include // float utilities /* STM32 (including CM3) libraries */ #include // Cortex M3 utilities @@ -24,6 +25,7 @@ #include // debug utilities #include // design utilities #include // flash utilities +#include // external interrupt defines /* own libraries */ #include "global.h" // board definitions @@ -32,6 +34,7 @@ #include "usb_cdcacm.h" // USB CDC ACM utilities #include "terminal.h" // handle the terminal interface #include "menu.h" // menu utilities +#include "lcd_hd44780.h" // LCD display /** watchdog period in ms */ #define WATCHDOG_PERIOD 10000 @@ -49,6 +52,24 @@ static volatile bool second_flag = false; /**< flag set when a second passed */ /** number of seconds since boot */ static uint32_t boot_time = 0; +/** connection to AD9850 */ +#define AD9850_DATA PA7 +#define AD9850_FQUD PA6 +#define AD9850_WCLK PA5 +//#define AD9850_D0 PA0 +//#define AD9850_D1 PA1 +//#define AD9850_D2 PA2 + +/** AD9850 frequency to set (in mHz) */ +static uint64_t ad9850_freq = 0; +/** maximum frequency (in mHz) */ +#define AD9850_MAX_FREQ 125000000000ULL + +/** connections to rotary encoder (common is ground) */ +#define ROTARY_A PA1 +#define ROTARY_B PA2 +static volatile int8_t rotary_flag = 0; /** flag set when rotary encoder is turned (1 = CW, -1 = CCW) */ + size_t putc(char c) { size_t length = 0; // number of characters printed @@ -267,6 +288,75 @@ static void command_bootloader(void* argument) dfu_bootloader(); // start DFU bootloader } +/** set AD9850 output frequency + * @param[in] frequency frequency to set (in Hz) + * @return actual frequency set (in Hz) + * @note a frequency of 0 disables the output + */ +static double ad9850_set_freq(double frequency) +{ + if (frequency > AD9850_MAX_FREQ / 1000) { + frequency = AD9850_MAX_FREQ / 1000; + } else if (frequency < 0) { + frequency = 0; + } + + // start with default state + gpio_clear(GPIO_PORT(AD9850_WCLK), GPIO_PIN(AD9850_WCLK)); + gpio_clear(GPIO_PORT(AD9850_FQUD), GPIO_PIN(AD9850_FQUD)); + + // enable serial mode (W0 must we xxxxx011, D0=1, D1=1, D2=0) + gpio_set(GPIO_PORT(AD9850_WCLK), GPIO_PIN(AD9850_WCLK)); + sleep_us(1); // tWH = 3.5 ns + gpio_clear(GPIO_PORT(AD9850_WCLK), GPIO_PIN(AD9850_WCLK)); + gpio_set(GPIO_PORT(AD9850_FQUD), GPIO_PIN(AD9850_FQUD)); + sleep_us(1); // tFH = 7 ns + gpio_clear(GPIO_PORT(AD9850_FQUD), GPIO_PIN(AD9850_FQUD)); + + // shift out data + const uint32_t freq = round(frequency * 0xffffffff / 125E6); // output 100 kHz + const uint8_t control = 0; // must be 0 for serial data + uint8_t power_down = 0; // power up + if (0 == frequency) { + power_down = 1; + } + const uint8_t phase = 0; + uint64_t shift_out = ((uint64_t)freq << 0) | ((uint64_t)control << 32) | ((uint64_t)power_down << 34) | ((uint64_t)phase << 35); // data to be shifted out + for (uint8_t b = 0; b < 40; b++) { // shift out data, LSb first + if (shift_out & 0x01) { + gpio_set(GPIO_PORT(AD9850_DATA), GPIO_PIN(AD9850_DATA)); + } else { + gpio_clear(GPIO_PORT(AD9850_DATA), GPIO_PIN(AD9850_DATA)); + } + sleep_us(1); // tDS = 3.5 ns + gpio_set(GPIO_PORT(AD9850_WCLK), GPIO_PIN(AD9850_WCLK)); + sleep_us(1); // tWH = 3.5 ns + gpio_clear(GPIO_PORT(AD9850_WCLK), GPIO_PIN(AD9850_WCLK)); + sleep_us(1); // tWL = 3.5 ns + // tDH = 3.5ns + shift_out >>= 1; // prepare next bit + } + + // latch data + // tFD = 7.0 ns + gpio_set(GPIO_PORT(AD9850_FQUD), GPIO_PIN(AD9850_FQUD)); + sleep_us(1); // tFH = 7.0 ns + gpio_clear(GPIO_PORT(AD9850_FQUD), GPIO_PIN(AD9850_FQUD)); + sleep_us(1); // tFL = 7.0 ns + + return freq * (125E6 / 0xffffffff); +} + +/** set AD9850 output frequency */ +static void command_freq(void* argument) +{ + if (argument) { + ad9850_freq = *(double*)argument * 1000.0; // get user provided frequency + } + const double freq = ad9850_set_freq(ad9850_freq / 1000.0); // set frequency and get the one set + printf("frequency set to %0.3f Hz\n", freq); +} + /** list of all supported commands */ static const struct menu_command_t menu_commands[] = { { @@ -325,6 +415,14 @@ static const struct menu_command_t menu_commands[] = { .argument_description = NULL, .command_handler = &command_bootloader, }, + { + .shortcut = 'f', + .name = "frequency", + .command_description = "set output frequency", + .argument = MENU_ARGUMENT_FLOAT, + .argument_description = "[Hz]", + .command_handler = &command_freq, + }, }; static void command_help(void* argument) @@ -356,6 +454,96 @@ static void process_command(char* str) } } +/** create 16 char representation of number + * @number number to represent + * @return 16 char representation + */ +static char* freq2s(uint64_t freq) +{ + static char line[16 + 1]; + for (uint8_t i = 0; i < LENGTH(line) - 1; i++) { + line[i] = ' '; // clear line + } + line[LENGTH(line) - 1] = '\0'; // terminate string + bool zero_padding = false; + uint64_t divider = 100000000000UL; + uint8_t pos = 1; // position in the line + for (uint8_t d = 0; d < 12; d++) { + if (3 == d || 6 == d) { + if (zero_padding) { + line[pos] = ','; // add separator + } + pos++; + } else if (9 == d) { + if (zero_padding) { + line[pos] = '.'; // add separator + } + pos++; + } + if (8 == d) { + zero_padding = true; // enforce hertz unit display + } + const uint8_t digit = (freq / divider) % 10; + if (digit > 0) { + line[pos] = '0' + digit; // set digit + zero_padding = true; // remember to pad with zeros now + } else if (zero_padding) { + line[pos] = '0'; + } else { + line[pos] = ' '; + } + divider /= 10; // go to next digit + pos++; // go to next position + } + return line; +} + +static void update_display(uint64_t freq, uint8_t position, bool selected) +{ + const uint8_t position2cursor_lut[] = {0x47, 0x46, 0x45, 0x43, 0x42, 0x41, 0x07, 0x06, 0x05, 0x03, 0x02, 0x01}; + if (position >= LENGTH(position2cursor_lut)) { + position = LENGTH(position2cursor_lut) - 1; + } + const char* line = freq2s(freq); // get frequency representation + lcd_hd44780_write_line(0, &line[0], 8); // display set frequency + lcd_hd44780_write_line(1, &line[8], 8); // display set frequency + lcd_hd44780_set_ddram_address(position2cursor_lut[position]); // set cursor position + lcd_hd44780_display_control(true, true, selected); +} + +/** load settings from SRAM */ +static void load_settings(uint8_t* position, uint64_t* frequency) +{ + if (position) { + *position = RTC_BKPXR(0); + if (*position >= 12) { + *position = 11; + } + } + + if (frequency) { + *frequency = (RTC_BKPXR(1) << 0) + ((uint64_t)RTC_BKPXR(2) << 32); + if (*frequency > AD9850_MAX_FREQ) { + *frequency = AD9850_MAX_FREQ; + } + } +} + +/** save settings to SRAM */ +static void save_settings(uint8_t position, uint64_t frequency) +{ + if (position >= 12) { + position = 11; + } + RTC_BKPXR(0) = position; + + if (frequency > AD9850_MAX_FREQ) { + frequency = AD9850_MAX_FREQ; + } + RTC_BKPXR(1) = frequency >> 0; + RTC_BKPXR(2) = frequency >> 32; +} + /** program entry point * this is the firmware function started by the micro-controller */ @@ -379,7 +567,7 @@ void main(void) board_setup(); // setup board uart_setup(); // setup USART (for printing) usb_cdcacm_setup(); // setup USB CDC ACM (for printing) - puts("\nwelcome to the CuVoodoo STM32F4 example firmware\n"); // print welcome message + puts("\nwelcome to the CuVoodoo clock generator\n"); // print welcome message #if DEBUG // show reset cause @@ -448,6 +636,43 @@ void main(void) // important: do not re-enable backup_domain_write_protect, since this will prevent clearing flags (but RTC registers do not need to be unlocked) puts_debug("OK\n"); + puts_debug("setup rotary encoder: "); + rcc_periph_clock_enable(GPIO_RCC(ROTARY_B)); // enable clock for button + gpio_mode_setup(GPIO_PORT(ROTARY_B), GPIO_MODE_INPUT, GPIO_PUPD_PULLUP, GPIO_PIN(ROTARY_B)); // set GPIO to input and pull up + rcc_periph_clock_enable(GPIO_RCC(ROTARY_A)); // enable clock for button + gpio_mode_setup(GPIO_PORT(ROTARY_A), GPIO_MODE_INPUT, GPIO_PUPD_PULLUP, GPIO_PIN(ROTARY_A)); // set GPIO to input and pull up + exti_select_source(GPIO_EXTI(ROTARY_A), GPIO_PORT(ROTARY_A)); // mask external interrupt of this pin only for this port + exti_set_trigger(GPIO_EXTI(ROTARY_A), EXTI_TRIGGER_FALLING); // trigger when button is pressed + exti_enable_request(GPIO_EXTI(ROTARY_A)); // enable external interrupt + nvic_enable_irq(GPIO_NVIC_EXTI_IRQ(ROTARY_A)); // enable interrupt + uint8_t digit_position = 0; // which digit is selected + load_settings(&digit_position, NULL); // load saved position + bool digit_selected = false; // if a digit is selected + puts_debug("OK\n"); + + puts_debug("setup AD9850: "); + rcc_periph_clock_enable(GPIO_RCC(AD9850_DATA)); // enable clock for GPIO + gpio_clear(GPIO_PORT(AD9850_DATA), GPIO_PIN(AD9850_DATA)); // don't care about data + gpio_mode_setup(GPIO_PORT(AD9850_DATA), GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN(AD9850_DATA)); // set pin as output + gpio_set_output_options(GPIO_PORT(AD9850_DATA), GPIO_OTYPE_PP, GPIO_OSPEED_2MHZ, GPIO_PIN(AD9850_DATA)); // set output as push-pull + rcc_periph_clock_enable(GPIO_RCC(AD9850_WCLK)); // enable clock for GPIO + gpio_clear(GPIO_PORT(AD9850_WCLK), GPIO_PIN(AD9850_WCLK)); // idle low + gpio_mode_setup(GPIO_PORT(AD9850_WCLK), GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN(AD9850_WCLK)); // set pin as output + gpio_set_output_options(GPIO_PORT(AD9850_WCLK), GPIO_OTYPE_PP, GPIO_OSPEED_2MHZ, GPIO_PIN(AD9850_WCLK)); // set output as push-pull + rcc_periph_clock_enable(GPIO_RCC(AD9850_FQUD)); // enable clock for GPIO + gpio_clear(GPIO_PORT(AD9850_FQUD), GPIO_PIN(AD9850_FQUD)); // idle low + gpio_mode_setup(GPIO_PORT(AD9850_FQUD), GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN(AD9850_FQUD)); // set pin as output + gpio_set_output_options(GPIO_PORT(AD9850_FQUD), GPIO_OTYPE_PP, GPIO_OSPEED_2MHZ, GPIO_PIN(AD9850_FQUD)); // set output as push-pull + load_settings(NULL, &ad9850_freq); // load saved frequency + ad9850_set_freq(ad9850_freq); + puts_debug("OK\n"); + + puts_debug("setup HD44780 LCD: "); + lcd_hd44780_setup(true, false); // I don't know why the initialisation does not always works the first time (but replugging the power in solveds it) + update_display(ad9850_freq, digit_position, digit_selected); + puts_debug("OK\n"); + + load_settings(&digit_position, &ad9850_freq); // setup terminal terminal_prefix = ""; // set default prefix terminal_process = &process_command; // set central function to process commands @@ -467,16 +692,62 @@ void main(void) } if (button_flag) { // user pressed button action = true; // action has been performed - puts("button pressed\n"); - led_toggle(); // toggle LED + sleep_ms(10); // debounce + if (!gpio_get(GPIO_PORT(BUTTON_PIN), GPIO_PIN(BUTTON_PIN))) { // only allow press (not release) + digit_selected = !digit_selected; + update_display(ad9850_freq, digit_position, digit_selected); + } sleep_ms(100); // wait a bit to remove noise and double trigger button_flag = false; // reset flag } + if (rotary_flag) { // user turned rotary encoder + action = true; // action has been performed + if (rotary_flag > 0) { + if (digit_selected) { + uint64_t unit = 1; + for (uint8_t i = 0; i < digit_position; i++) { + unit *= 10; + } + ad9850_freq += unit; + if (ad9850_freq > AD9850_MAX_FREQ) { + ad9850_freq = AD9850_MAX_FREQ; + } + } else { + if (digit_position > 0) { + digit_position--; + } + } + } else { + if (digit_selected) { + uint64_t unit = 1; + for (uint8_t i = 0; i < digit_position; i++) { + unit *= 10; + } + if (unit > ad9850_freq) { + ad9850_freq = 0; + } else { + ad9850_freq -= unit; + } + } else { + if (digit_position < 11) { + digit_position++; + } + } + } + save_settings(digit_position, ad9850_freq); // save setting to SRAM + ad9850_set_freq(ad9850_freq / 1000.0); // reset frequency (in case the target has been reset) + update_display(ad9850_freq, digit_position, digit_selected); + sleep_ms(100); // wait a bit to remove noise and double trigger + rotary_flag = 0; // reset flag + } if (wakeup_flag) { // time to do periodic checks wakeup_flag = false; // clear flag } if (second_flag) { // one second passed second_flag = false; // clear flag + action = true; // action has been performed + ad9850_set_freq(ad9850_freq / 1000.0); // reset frequency (in case the target has been reset) + update_display(ad9850_freq, digit_position, digit_selected); led_toggle(); // toggle LED to indicate if main function is stuck } if (action) { // go to sleep if nothing had to be done, else recheck for activity @@ -500,3 +771,16 @@ void rtc_wkup_isr(void) tick = WAKEUP_FREQ; // restart count down } } + +/** interrupt service routine called when rotary encoder is turned */ +void GPIO_EXTI_ISR(ROTARY_A)(void) +{ + exti_reset_request(GPIO_EXTI(ROTARY_A)); // reset interrupt + if (rotary_flag) { // flag not cleared yet + return; + } else if (gpio_get(GPIO_PORT(ROTARY_B), GPIO_PIN(ROTARY_B))) { + rotary_flag = 1; + } else { + rotary_flag = -1; + } +} diff --git a/lib/usb_cdcacm.c b/lib/usb_cdcacm.c index 88c7cbf..4f77367 100644 --- a/lib/usb_cdcacm.c +++ b/lib/usb_cdcacm.c @@ -229,7 +229,7 @@ static char usb_serial[] = "0123456789ab"; */ static const char* usb_strings[] = { "CuVoodoo", /**< manufacturer string */ - "CuVoodoo STM32F4xx firmware", /**< product string */ + "CuVoodoo clock generator", /**< product string */ (const char*)usb_serial, /**< device ID used as serial number */ "DFU bootloader (runtime mode)", /**< DFU interface string */ };