模拟电路工程化设计 第二部分 任意波形发生器
实现功能
-
使用ESP32S3单片机 通过SPI控制ad5626产生正弦波、三角波、锯齿波、方波
-
信号输出幅度3Vpp
-
实现波形直流偏移可调1.5V-3.5V
-
实现一个Sallen-Key低通滤波器
实物展示
系统框图
硬件实现
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_val
从0
遍历到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 = C2
, Vo / Vin
的响应就是一阶的巴特沃斯滤波器。下图中320ohm的电阻通过套件中的1000ohm和470ohm并联获得。负电压-5V可以使用套件中的电荷泵LT1054产生。计算出的-3dB带宽为32.2kHz。可以认为接近题目要求的40kHz。
实验结果
- 三角波
- 锯齿波
- 方波
- 100Hz 正弦波
- 1kHz 正弦波
- 10kHz 正弦波
- 1kHz 正弦波 偏置 0V
- 1kHz 正弦波 偏置 0.5V
- 1kHz 正弦波 偏置 1.5V
- 1kHz 正弦波 偏置 2.0V
Sallen-Key滤波器
手头没有能测这个滤波器波特图的仪器,简单看一下示波器的FFT,可以看到40kHz以后的峰值被明显削弱。
- Sallen-key 滤波前 FFT
- Sallen-key 滤波后 FFT
遇到的问题
-
原先选择了rp2040+micropython开发,但是micropython容易卡死,代码报错的时候无法进入控制台,只能每次重刷uf2的micropython固件。而且micropython平台定时器的中断触发事件最短只有1ms,速度太慢,于是更换了ESP32S3。但是也遇到了代码执行速度不够快的问题,定时器中断间隔设置为20us是一个比较合适的值,再小会一直触发定时器中断而没有办法进入其他逻辑代码。目前无法跑到20kHz,最大跑到10kHz,在10kHz下一个周期也只有5个点。可以优化代码,减小其他业务逻辑的计算量。或者更换单片机,选择使用STM32或者选择FPGA。
-
现在使用ESP32S3产生波形,相位稳定性很差,在示波器上显示为波形会左右抖动。代码上可以优化,但是我不知道怎么优化,如果要改进可以看看别人的代码怎么写的。或者可以换FPGA。
-
使用面包板搭建电路,没有做模拟电源和数字电源的分割。模拟信号中会引入数字信号的噪声。可以画个四层电路板,获得纯净的模拟电源。出来的信号也自然会更干净。
总结
这个项目软硬件同时考虑,软件代码会影响模拟电路的性能参数,代码能力还需要加强。同时我更加理解了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);
}
}