模拟电路工程化设计 第二部分 任意波形发生器
1. 使用ESP32S3单片机 通过SPI控制ad5626产生正弦波、三角波、锯齿波、方波 2. 信号输出幅度3Vpp 3. 实现波形直流偏移可调1.5V-3.5V 4. 实现一个Sallen-Key低通滤波器
标签
嵌入式系统
ESP32
DAC
模拟电路
模拟电路工程化设计课程
Sallen-Key
hucuyuu
更新2023-05-09
548

模拟电路工程化设计 第二部分 任意波形发生器

实现功能

  1. 使用ESP32S3单片机 通过SPI控制ad5626产生正弦波、三角波、锯齿波、方波

  2. 信号输出幅度3Vpp

  3. 实现波形直流偏移可调1.5V-3.5V

  4. 实现一个Sallen-Key低通滤波器

实物展示

Fj5-c2aPABWOMDNwu_YmgqMNvuAMFhM_qII9rUM6ak5D68ayXY-IzGpHFrwcPXNhRvzZPuHBG8Mrn0s4pyv3

 

 

系统框图

硬件实现

1. dac驱动(ad5626)

单片机使用合宙ESP32-S3平台,ESP-IDF平台开发。首先考虑创建一个定时器,每20us触发一次定时器中。在中断中将提前算好的波形表通过SPI接口送到ad5626。 信号的周期T = 1 / f,定时器中断世间t_intr = 20us, 计算得到波形表总长度L = T / t_intr。在下面的公式中raw_val是波表,AMP_DAC是dac在SPI中信号的幅值,这里ad5626是12bit分辨率的dac,满量程输出为4.095V,对应的信号为0xfff,即十进制的4095,项目要求3V的幅值,AMP_DAC设置为3000。若要更改波形的幅值,可以更改AMP_DAC的值。

正弦信号的波表为raw_val[i] = ((sin(i * 2pi / L) + 1) * AMP_DAC / 2 + 0.5);

三角波的波表为raw_val[i] = (i > (L / 2)) ? (2 * AMP_DAC * (L- i) / L) : (2 * AMP_DAC * i / L);

锯齿波的波表为raw_val[i] = (i == L) ? 0 : (i * AMP_DAC / L);

方波的波表为raw_val[i] = (i < (L / 2)) ? AMP_DAC : 0;

中断中设置将波表raw_val0遍历到L,就可以输出一个完整周期的波形。单片机方便地设置定时器中断的触发时间,所以这里不考虑FPGA中使用的相位累加器,代码上更加简单。

2. 直流偏置

单片机产生一个PWM,PWM波形的最小值为0V,最大值为3.3V。由傅里叶分析可以知道PWM包含了一个直流分量和谐波分量,用滤波器消除谐波分量的就可以获得直流量。单片机输出直流偏置的值 = 占空比 * 3.3V。在此项目中使用一个截止频率79.6Hz的RC低通滤波器,这里PWM为10kHZ,截至频率要求上远远满足要求。但是截止频率不是越小越好,截止频率越小,时间常数tao = R * C越大,RC电路充电时间就越长,在单片机动态调整直流偏置时,瞬态响应变差,需要更长的时间达到目标的偏置值。

3. 加法电路

这里选择ad8542轨道轨运放,单电源供电,所以使用同相比例相加电路。先用一个电压跟随器将滤波后的直流偏置电压进行阻抗变换,若直接与后面的R3连接,R3会与前面的RC网络分压,会导致相加的电压不准确。运放同相端电压Vp = V3 * R2(R2 + R3) + V4* R3(R2 + R3)。运放输出电压Vo = Vp*(1 + R4 / R5)。在这里选择R2 = R3 = R4 = R5 = 10kohm,所以Vo = V3 + V4

4. Sallen-Key滤波器

Sallen-Key滤波器的原理可以参考ADI的应用笔记MT222,在这种结构中,滤波器的性能对运算放大器性能的依赖程度最低。由于运算放大器配置为放大器而非积分器,因此最大限度降低了其增益带宽积的要求。也可以直接选择使用ADI的设计工具,输入需要的通带和阻带,还有滤波器响应,可以自动设计滤波器的级数和电阻电容的参数。

但是ADALP2000的套件中没有那么多种类的电容和电阻。现在使用一种更简单的方法设计Sallen-key滤波器。我们选择R3 = 0,R1 = R2, C1 = C2Vo / Vin的响应就是一阶的巴特沃斯滤波器。下图中320ohm的电阻通过套件中的1000ohm和470ohm并联获得。负电压-5V可以使用套件中的电荷泵LT1054产生。计算出的-3dB带宽为32.2kHz。可以认为接近题目要求的40kHz。

实验结果

  • 三角波

FpX9okiB5ZpGe6VOXyjlfRRstL28

  • 锯齿波

Fn0BK0EBxkwoeboxsY-IF4Anj2dm

  • 方波

Fv_ZDvFjfs-0LUJn99d7miPdkkTI

  • 100Hz 正弦波

FjaIdh-mY0MXSWGGQWApfSNyhw0_

  • 1kHz 正弦波

FvAPb0UW6D-PMu8jSfXyF5j02WrS

  • 10kHz 正弦波

FjQ51VJ9kYNnyOZCmJjTd7TrvMTE

  • 1kHz 正弦波 偏置 0V

Fr2EIs1foyHtzHkJE-pcE-Yoe2_6

  • 1kHz 正弦波 偏置 0.5V

FixV8FzYQwfZW7ZleycIX9E3NF27

  • 1kHz 正弦波 偏置 1.5V

FsmShapp52lV-bzQE4swmmIMnrJv

  • 1kHz 正弦波 偏置 2.0V

FsntzzP_zmG99IW7ZYP9eSMVmTTF

Sallen-Key滤波器

手头没有能测这个滤波器波特图的仪器,简单看一下示波器的FFT,可以看到40kHz以后的峰值被明显削弱。

  • Sallen-key 滤波前 FFT

FpqYks2pll9p_dXuVBZEVpJP1ihA

  • Sallen-key 滤波后 FFT

FnXk719IflDHM9wVSRpricdWow7a

遇到的问题

  1. 原先选择了rp2040+micropython开发,但是micropython容易卡死,代码报错的时候无法进入控制台,只能每次重刷uf2的micropython固件。而且micropython平台定时器的中断触发事件最短只有1ms,速度太慢,于是更换了ESP32S3。但是也遇到了代码执行速度不够快的问题,定时器中断间隔设置为20us是一个比较合适的值,再小会一直触发定时器中断而没有办法进入其他逻辑代码。目前无法跑到20kHz,最大跑到10kHz,在10kHz下一个周期也只有5个点。可以优化代码,减小其他业务逻辑的计算量。或者更换单片机,选择使用STM32或者选择FPGA。

  2. 现在使用ESP32S3产生波形,相位稳定性很差,在示波器上显示为波形会左右抖动。代码上可以优化,但是我不知道怎么优化,如果要改进可以看看别人的代码怎么写的。或者可以换FPGA。

  3. 使用面包板搭建电路,没有做模拟电源和数字电源的分割。模拟信号中会引入数字信号的噪声。可以画个四层电路板,获得纯净的模拟电源。出来的信号也自然会更干净。

总结

这个项目软硬件同时考虑,软件代码会影响模拟电路的性能参数,代码能力还需要加强。同时我更加理解了DAC, 运放, Sallen-Key滤波器的使用。熟练使用了LTSpice仿真电路,仿真后再搭建电路。

附件中有可以编译的ESP工程代码和三个仿真。


代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <assert.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"

#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "driver/gptimer.h"
#include "driver/ledc.h"

#include "sdkconfig.h"
#include "esp_log.h"

static const char TAG[] = "main";

#define PIN_NUM_nCS 1
#define PIN_NUM_CLK 2
#define PIN_NUM_MOSI 3
#define PIN_NUM_nLDAC 4
#define PIN_NUM_nCLR 5
#define PIN_NUM_DC_BIAS 6
#define AD5626_HOST SPI2_HOST
static spi_device_handle_t spi;

void spi_ad5626_init()
{
        esp_err_t ret;
        ESP_LOGI(TAG, "Initializing bus SPI...");

        spi_bus_config_t buscfg = {
            .miso_io_num = -1,
            .mosi_io_num = PIN_NUM_MOSI,
            .sclk_io_num = PIN_NUM_CLK,
            .quadwp_io_num = -1,
            .quadhd_io_num = -1,
            .max_transfer_sz = 32,
        };
        spi_device_interface_config_t devcfg = {
            .clock_speed_hz = 20 * 1000 * 1000,
            .command_bits = 0,
            .address_bits = 0,
            .mode = 3, // CPOL=1, CPHA=1
            .spics_io_num = PIN_NUM_nCS,
            .queue_size = 7,
        };

        // Initialize the SPI bus
        ret = spi_bus_initialize(AD5626_HOST, &buscfg, SPI_DMA_CH_AUTO);
        ESP_ERROR_CHECK(ret);
        ret = spi_bus_add_device(AD5626_HOST, &devcfg, &spi);
        ESP_ERROR_CHECK(ret);

        // init gpio
        gpio_config_t io_conf = {};
        io_conf.mode = GPIO_MODE_OUTPUT;
        io_conf.pin_bit_mask = ((1ULL << PIN_NUM_nLDAC) | (1ULL << PIN_NUM_nCLR));
        io_conf.pull_down_en = 0;
        io_conf.pull_up_en = 1;
        gpio_config(&io_conf);

        gpio_set_level(PIN_NUM_nCLR, 0);
        gpio_set_level(PIN_NUM_nCLR, 1);
        gpio_set_level(PIN_NUM_nLDAC, 1);
}

void IRAM_ATTR spi_ad5626(uint16_t data)
{
        uint16_t tmp = 0;
        data = data << 4;
        tmp = ((data & 0x00ff) << 8) | ((data & 0xff00) >> 8);

        spi_transaction_t t = {
            .length = 12,
            .flags = 0,
            .tx_buffer = &tmp,
        };
        spi_device_polling_transmit(spi, &t);
        // gpio_set_level(PIN_NUM_nLDAC, 1);
        gpio_set_level(PIN_NUM_nLDAC, 0);
        gpio_set_level(PIN_NUM_nLDAC, 1);
}

#define TIMER_INTR_US 200 // Execution time of each ISR interval in 0.1us
#define POINT_ARR_LEN 500 // Length of points array
#define AMP_DAC 3000      // Amplitude of DAC voltage. If it's more than 256 will causes dac_output_voltage() output 0.
#define VDD 4095          // VDD
#define CONST_PERIOD_2_PI 6.2832
// #define FREQ 3000                                                       // 3kHz by default
// #define OUTPUT_POINT_NUM (int)(10000000 / (TIMER_INTR_US * FREQ) + 0.5) // The number of output wave points.

static uint32_t freq;
static uint32_t output_point_num;
// _Static_assert(OUTPUT_POINT_NUM <= POINT_ARR_LEN, "The CONFIG_EXAMPLE_WAVE_FREQUENCY is too low and using too long buffer.");

static int raw_val[POINT_ARR_LEN];  // Used to store raw values
static int volt_val[POINT_ARR_LEN]; // Used to store voltage values(in mV)
// static const char *TAG = "wave_gen";

static int g_index = 0;
/* Timer interrupt service routine */
static bool IRAM_ATTR on_timer_alarm_cb(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_data)
{
        int *head = (int *)user_data;

        /* DAC output ISR has an execution time of 4.4 us*/
        if (g_index >= output_point_num)
        {
                g_index = 0;
        }
        // dac_output_voltage(DAC_CHAN, *(head + g_index));
        spi_ad5626(*(head + g_index));
        g_index++;
        return false;
}

static void prepare_data(int pnt_num, int wave)
{
        for (int i = 0; i < pnt_num; i++)
        {
                switch (wave)
                {

                case 1: // sin wave
                        raw_val[i] = (int)((sin(i * CONST_PERIOD_2_PI / pnt_num) + 1) * (double)(AMP_DAC) / 2 + 0.5);
                        break;

                case 2: // trignal wave
                        raw_val[i] = (i > (pnt_num / 2)) ? (2 * AMP_DAC * (pnt_num - i) / pnt_num) : (2 * AMP_DAC * i / pnt_num);
                        break;

                case 3: // swatooth wave
                        raw_val[i] = (i == pnt_num) ? 0 : (i * AMP_DAC / pnt_num);
                        break;

                case 4: // square wave
                        raw_val[i] = (i < (pnt_num / 2)) ? AMP_DAC : 0;
                        break;

                default:
                        raw_val[i] = (i < (pnt_num / 2)) ? AMP_DAC : 0;
                        break;
                }

                volt_val[i] = (int)(VDD * raw_val[i] / (float)AMP_DAC);
        }
}

#define PIN_NUM_BOOT0 0
#define LEDC_TIMER LEDC_TIMER_0
#define LEDC_MODE LEDC_LOW_SPEED_MODE
#define LEDC_OUTPUT_IO (6) // Define the output GPIO
#define LEDC_CHANNEL LEDC_CHANNEL_0
#define LEDC_DUTY_RES LEDC_TIMER_10_BIT // Set duty resolution to 10 bits
#define LEDC_DUTY (511)                 // Set duty to 50%. ((2 ** 10) - 1) * 50% = 4095
#define LEDC_FREQUENCY (10000)          // Frequency in Hertz. Set frequency at 5 kHz
static uint32_t DC_duty = 0;

static void ledc_task()
{
        while (true)
        {
                DC_duty += 155;
                if (DC_duty == (620 + 155))
                {
                        DC_duty = 0;
                }
                ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL, DC_duty);
                ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL);
                vTaskDelay(1000 / portTICK_PERIOD_MS);
        }
}

static void dc_bias(uint16_t duty)
{
        ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL, duty);
        ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL);
}

static void example_ledc_init(void)
{

        // Prepare and then apply the LEDC PWM timer configuration
        ledc_timer_config_t ledc_timer = {
            .speed_mode = LEDC_MODE,
            .timer_num = LEDC_TIMER,
            .duty_resolution = LEDC_DUTY_RES,
            .freq_hz = LEDC_FREQUENCY, // Set output frequency at 5 kHz
            .clk_cfg = LEDC_AUTO_CLK};
        ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));

        // Prepare and then apply the LEDC PWM channel configuration
        ledc_channel_config_t ledc_channel = {
            .speed_mode = LEDC_MODE,
            .channel = LEDC_CHANNEL,
            .timer_sel = LEDC_TIMER,
            .intr_type = LEDC_INTR_DISABLE,
            .gpio_num = LEDC_OUTPUT_IO,
            .duty = LEDC_DUTY, // Set duty to 0%
            .hpoint = 0};
        ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));

        // xTaskCreate(ledc_task, "ledc_task", 2048, NULL, 10, NULL);
}

static void wave_change_task()
{
        uint8_t index = 1;
        while (true)
        {
                switch (index)
                {
                case 1:
                        dc_bias(0);
                        prepare_data(output_point_num, 1);
                        break;
                case 2:
                        prepare_data(output_point_num, 2);
                        break;
                case 3:
                        prepare_data(output_point_num, 3);
                        break;
                case 4:
                        prepare_data(output_point_num, 4);
                        break;

                case 5:
                        freq = 100;
                        output_point_num = (int)(10000000 / (TIMER_INTR_US * freq) + 0.5);
                        prepare_data(output_point_num, 1);
                        break;
                case 6:
                        freq = 1000;
                        output_point_num = (int)(10000000 / (TIMER_INTR_US * freq) + 0.5);
                        prepare_data(output_point_num, 1);
                        break;
                case 7:
                        freq = 10000;
                        output_point_num = (int)(10000000 / (TIMER_INTR_US * freq) + 0.5);
                        prepare_data(output_point_num, 1);
                        break;

                case 8: // 0.5V
                        freq = 1000;
                        output_point_num = (int)(10000000 / (TIMER_INTR_US * freq) + 0.5);
                        prepare_data(output_point_num, 1);
                        dc_bias(155);

                        break;

                case 9: // 1.0V
                        dc_bias(155 * 2);
                        break;

                case 10: // 1.5V
                        dc_bias(155 * 3);
                        break;
                case 11: // 2.0V
                        dc_bias(155 * 4);
                        break;
                default:
                        break;
                }

                index++;
                if (index == 12)
                {
                        index = 1;
                }
                vTaskDelay(1000 / portTICK_PERIOD_MS);
        }
}

void app_main(void)
{

        freq = 1000;
        output_point_num = (int)(10000000 / (TIMER_INTR_US * freq) + 0.5);
        prepare_data(output_point_num, 2);
        dc_bias(0);
        xTaskCreate(wave_change_task, "wave_change_task", 2048, NULL, 10, NULL);

        // Set DC bias.
        example_ledc_init();

        // Set ad5626 voltage in gptimer callback
        gptimer_handle_t gptimer = NULL;
        gptimer_config_t timer_config = {
            .clk_src = GPTIMER_CLK_SRC_DEFAULT,
            .direction = GPTIMER_COUNT_UP,
            .resolution_hz = 10 * 1000 * 1000, // 10MHz, 1 tick = 0.1us
        };
        ESP_ERROR_CHECK(gptimer_new_timer(&timer_config, &gptimer));
        spi_ad5626_init();

        gptimer_alarm_config_t alarm_config = {
            .reload_count = 0,
            .alarm_count = TIMER_INTR_US,
            .flags.auto_reload_on_alarm = true,
        };
        gptimer_event_callbacks_t cbs = {
            .on_alarm = on_timer_alarm_cb,
        };

        ESP_ERROR_CHECK(gptimer_register_event_callbacks(gptimer, &cbs, raw_val));
        ESP_ERROR_CHECK(gptimer_set_alarm_action(gptimer, &alarm_config));
        ESP_ERROR_CHECK(gptimer_enable(gptimer));
        ESP_ERROR_CHECK(gptimer_start(gptimer));

        while (1)
        {
                vTaskDelay(100);
        }
}
附件下载
LTSpice仿真.zip
三个仿真 1. PWM 2. 同相相加 3. Sallen-Key滤波器
esp32s3_esp-idf_v_5_0_1_dac.zip
ESP-IDF 版本 v5.0.1
团队介绍
hucuyuu
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号