基于RP2350B核心板设计DDS信号发生器
该项目使用了RP2350B核心板,实现了DDS信号发生器的设计,它的主要功能为:产生频率和幅度可调的正弦波、三角波、方波、直流,并直观展示波形及参数。
标签
数字逻辑
DDS
RP2350
PIO
TetraPak
更新2025-12-01
48

基于RP2350B核心板设计DDS信号发生器

项目背景

任务要求

  1. 能够产生正弦波、三角波、方波、直流,可以通过核心板的拨码开关控制波形的切换
  2. 产生信号的幅度0-3Vpp之间可调,可以通过电位计进行调节
  3. 产生信号的频率100Hz - 200KHz之间可调
  4. 产生的波形、波形的幅度、波形的频率都实时显示在OLED屏幕上

功能介绍

本项目旨在设计并实现一个基于RP2350B核心板的多功能信号发生器。该信号发生器能够产生多种基本波形,包括正弦波、三角波、方波以及直流(DC)信号。用户可以通过核心板上的拨码开关方便地切换所需的输出波形类型。此外,信号的峰峰值幅度(Vpp)可在 0 至 3V 范围内通过电位计进行连续调节,输出信号的频率同样支持在 100Hz 到 200KHz 的宽范围内调节。为了提供实时反馈,系统集成了一个 OLED 显示屏,用于实时显示当前生成的波形类型、实际输出幅度和频率值。

硬件介绍

本项目核心控制器采用 Raspberry Pi RP2350B,这是一款功能强大的微控制器,具有丰富的 GPIO 资源和独特的 PIO(可编程 I/O)模块。 相对于RP2040和RP2350A只有30个IO,RP2350B拥有48个IO,这增加的18个IO就可以通过合理的配置,可以访问更多的外设

PIO介绍

RP2350B上的 PIO(Programmable I/O)是一套独立于主 Cortex-M0+ 核心运行的可编程硬件逻辑。它相当于片上配置的小型协处理器,可以直接控制 GPIO 引脚,按照用户编写的指令序列产生精确的时序和电平变化,从而在单片机内部就实现高度定制的 I/O 行为。

使用 PIO 的最大意义在于把对时序要求严格或占用大量处理器时间的引脚任务从主核剥离出来。状态机一旦启动便自主执行指令,主核只需通过 FIFO 缓冲区与之交换数据,无需再被中断或轮询束缚,于是更多运算资源可以投向应用逻辑。每条 PIO 指令的长度固定为十六位,执行时间精确到一个系统时钟周期;在默认150Mz时钟下,执行一条指令仅花费6.7 ns。芯片内部包含三组 PIO 模块,每组配备四个独立状态机,可在同一时刻同时承担多路 I/O 任务。

例如本综合技能训练板的ADS7868和OLED,当硬件连线与既有外设资源不匹配时,PIO 用“纯软件 + 少量逻辑”就能把任何引脚变成自定义的通讯接口在许多实际应用里,板上的 SPI、I²C 或 UART 外设往往只映射在固定几组 GPIO 位置上,此时只需通过状态机把任意一组空闲 GPIO 重新塑造成所需要的接口,甚至完全自定义的时序;主核只需把要发出的字节塞进 TX FIFO,或从 RX FIFO 读取收到的数据,其他细节全部由 PIO 完成。

设计思路

主要困难

  1. 外设接口: OLED 屏幕和 ADS7868 ADC 的连接引脚并非 RP2350B 的原生硬件 SPI 外设引脚,需要寻找替代通信方式。
  2. 高位 GPIO 操作: RP2350B 的 GPIO 引脚编号大于 31 时,其寄存器控制方式与低位 GPIO 不同,标准 PICO SDK 函数可能不直接支持,需要适配。
  3. 精确波形生成: 在较宽的频率范围(高达 200kHz)内精确生成多种波形,特别是正弦波,对时序控制和计算效率要求高。
  4. 复合输入解码: 如何通过单个 ADC 通道读取多个拨码开关和按键的状态组合。

解决思路

核心思路1:利用 PIO 和软件模拟实现灵活 I/O

对于 ADS7868 ADC,利用 RP2350B 的 PIO 模块,编写 PIO 程序来精确模拟所需的 SPI 时序(CPOL=1, CPHA=1),使其可以在任意 GPIO 引脚上运行。

ADS7868 A/D 转换器通过与 SPI 兼容的高速串行总线,直接与MCU通信。它们采用

  • CPOL = 1:SCLK 空闲时保持高电平,
  • CPHA = 1:数据在 SCLK 的下降沿发生变化,并在随后的上升沿被主机采样。

当片选信号 CS 从高电平拉低时,转换器立即开始对输入信号采样、执行 A/D 转换,并激活串行输出引脚 SDO串行时钟 SCLK 不仅用来移出转换后的数字数据,还能让主机处理器与转换器在同一时钟边沿上同步,从而精确控制转换速率。

A/D 转换器的 CSSCLK 这两个数字输入可承受高于自身电源电压(Vₙₙ)但不超过 3.6 V 的电平(即最大 Vᴵᴴ = 3.6 V)这样一来,即便主机使用了不同于转换器的供电电压,也能直接对接,无需外部电平转换器。

每次转换结束后,SDO 会依次输出 4 个前导零,然后跟上 12/10/8 位 的转换结果(直接二进制格式)。

具体时序是:

  • CS 下降后,第一个前导零就可在 SDO 上读到,直到 SCLK 的第一次下降沿;
  • 紧接着的第 1、2、3 次 SCLK 下降沿各移出一个前导零;
  • 第 4 次 SCLK 下降沿开始,输出结果的最高位(MSB);
  • 当转换完成(EOC 信号)时,无论是 CS 上升沿还是 SCLK 的某次下降沿,都会将 SDO 拉入三态输出。

根据数据手册关于时序的描述,设计时序如下:
在 MCU 端,需要先将片选信号 CS 拉低以激活转换器,然后以 CPOL=1、CPHA=1 的 SPI 时序模式生成连续 12 个 SCLK 周期;在第 1 至 3 次下降沿上,SDO 上输出的是前导零,无需采样,从第 4 到第 11 次下降沿,MCU 按照 MSB→LSB 顺序在每次下降沿处读取一位转换结果的有效数据;第 12 次下降沿后,SDO 自动进入三态,无需采样;最后,将 CS 拉高以结束本次转换。


对于 OLED 显示屏,采用软件模拟 SPI的方式,通过精确控制 GPIO 翻转和延时 (delay_cycles) 来驱动。

核心思路2:适配 RP2350B 高位 GPIO 操作

  1. 修改 Pico SDK 头文件 (eetree_rp2350b.h),识别 RP2350B 型号。在C SDK中board文件夹中复制pico.h,重命名为eetree_rp2350b.h文件,将#define PICO_RP2350A 1改为#define PICO_RP2350A 0。
  2. 使用 gpio_put_masked64 函数来操作包含高位 GPIO(>31)的引脚组,如 R-2R DAC 输出(GP28-GP37)和 OLED 控制引脚(GP41-GP45)。
  3. 补充实现 gpio_init_mask64 函数,用于批量初始化包含高位 GPIO 的引脚。

核心思路3:采用 DDS 技术生成波形

  1. 采用直接数字频率合成(DDS)技术。核心是维护一个 32 位相位累加器 (phase_accumulator),每次更新时增加一个相位增量 (phase_increment)。相位增量的大小决定了输出频率。
  2. 对于正弦波,使用查找表(LUT, sine_lut_quarter)。只存储 1/4 周期的采样点,通过相位累加器的高位判断当前所处的象限和在象限内的地址,结合对称性计算出完整的正弦波采样值。
  3. 对于三角波方波,直接根据相位累加器的高位进行逻辑判断生成,无需大型查找表,计算量小。
  4. 对于直流,直接输出由幅度控制决定的固定值。

波形数据最终通过 R-2R DAC 网络转换为模拟信号。对于使用R2R网络生成任意波形的,这里参考了硬禾的DDS生成任意波形的方法,用R-2R构成的低成本DAC来实现。

在硬件结构上,R-2R输出电压通过电压跟随器与负载隔离。

为了产生任意波形,DDS依赖两个技巧: LUT - Lookup Table(查找表)和 相位累加器

  • 6 位索引(0–63)
    base_addr 保存了一个范围在 0 到 63 之间的值,表示第一个 90 度象限内的特定偏移或索引。
  • 查找表内容
    sine_lut_quarter 数组只存储了第一个象限(0–90°)的正弦波形状,共 64 个采样点。
  • 定位采样点
    base_addr 告诉你要从这 64 个采样点中选择哪一个,来表示相对于该象限起始角度的当前角度。

输出频率 foutf_fout、相位累加器位宽 NNN、相位增量值 phase_inc 与累加器更新速率fupdate 之间的关系为:

更新速率 f_update主要由 DELAY_US 循环延迟加上循环内代码的执行时间决定。假设循环内代码执行时间相对于 DELAY_US 而言可以忽略(尤其是当 DELAY_US > 0 时),可以近似认为:

因此,将公式变形求相位增量 phase_inc

核心思路4:ADC 结合电阻网络与差值匹配解码复合输入

将拨码开关和按键通过不同的电阻连接到同一个 ADC 输入引脚,构成一个分压网络。不同的开关/按键组合会产生不同的电压值。

预先测量或计算出每个独立开关/按键按下时 ADC 读数相对于基准值(无按键按下时)的差值

读取 ADC 值后,进行滤波处理,计算与基准值的差值。

遍历所有可能的有效按键组合(考虑约束条件,如不允许某些开关按下、限制同时按下的数量),计算理论差值总和,并与实际测量的差值在一定容差范围内进行比较,从而解码出当前按下的开关/按键状态掩码。

系统功能结构框图

核心配置及代码片段

PIO SPI 配置 (for ADS7868):

pio初始化:


pio_spi_inst_t spi = {
.pio = pio0,
.sm = 0,
.cs_pin = ADS7868_PIN_CS
};

uint offset = pio_add_program(spi.pio, &spi_cpha1_program); // Load PIO program
pio_spi_init(spi.pio, spi.sm, offset,
12, // 12 bits per SPI frame for ADS7868
37.5f, // Clock divider for desired SPI frequency (e.g., 1 MHz)
true, // CPHA = 1
true, // CPOL = 1
ADS7868_PIN_SCLK,
ADS7868_PIN_SDO
);

// Reading data using PIO SPI
ads7868_read(&spi, buf, 1); // buf[0] will contain the 12-bit reading


ads7868 pio时序配置:

.program spi_cpha1
.side_set 1


; Clock phase = 1: data transitions on the leading edge of each SCK pulse, and
; is captured on the trailing edge.


    ; out x, 1    side 0     ; Stall here on empty (keep SCK deasserted)
    ; mov pins, x side 1 [1] ; Output data, assert SCK (mov pins uses OUT mapping)
    ; in pins, 1  side 0     ; Input data, deassert SCK
    out NULL, 1     side 0     ; Stall here on empty (keep SCK deasserted)
    nop side 1 [1] ; Output data, assert SCK (mov pins uses OUT mapping)
    in pins, 1  side 0     ; Input data, deassert SCK


SPI精确延时(for SSD1306 OLED):

由于oled屏幕的接口未接在硬件SPI外设上,对于oled屏的驱动,参考了综合技能训练平台的128*32 OLED显示屏例程来编写,用软件翻转IO口来模拟spi时序,不过这里用了更精确的延时方式,让 CPU 空转,以拉开时钟翻转之间的时间间隔,进而控制SPI 的时钟频率。

static inline void delay_cycles(uint32_t cycles) {
    for (volatile uint32_t i = 0; i < cycles / 4; ++i) { // 假设循环体约占4个周期
         __asm volatile (""); // 空汇编,防止循环被完全优化掉
    }
}

DDS 相位增量计算:

// f_{out} = (f_{update} * phase_inc) / 2^N
// phase_inc = (f_{out} * 2^N) / f_{update}
// where N = PHASE_ACCUMULATOR_BITS, f_update ≈ 1 / loop_time_seconds
uint32_t dds_calculate_phase_increment(float frequency) {
uint64_t numerator = (uint64_t)frequency * (1ULL << PHASE_ACCUMULATOR_BITS);
uint64_t denominator = 0;
if (DELAY_US == 0) { // Estimate update frequency if no explicit delay
const float estimated_f_update = 250000.0f; // Example estimate
denominator = (uint64_t)estimated_f_update;
} else { // Calculate from delay
denominator = 1000000ULL / DELAY_US;
}
if (denominator == 0) return 0;
return (uint32_t)(numerator / denominator);
}
// Update phase accumulator in main loop
phase_accumulator += phase_increment;


正弦波 LUT 查找逻辑:





case WAVEFORM_SINE: {
uint8_t sine_phase_8bit = (phase_accumulator >> get_sine_shift()) & 0xFF;
uint8_t quadrant = (sine_phase_8bit >> 6) & 0x03; // Top 2 bits for quadrant
uint8_t base_addr = sine_phase_8bit & SINE_LUT_ADDRESS_MASK; // Lower 6 bits for address within quadrant
// Adjust address based on quadrant for symmetry
uint8_t lut_addr = (quadrant == 1 || quadrant == 3) ? ((~base_addr) & SINE_LUT_ADDRESS_MASK) : base_addr;
int16_t lut_value = sine_lut_quarter[lut_addr]; // Get value from 1/4 LUT
// Apply sign based on quadrant
int16_t ideal_deviation = (quadrant == 0 || quadrant == 1) ? lut_value : -lut_value;
// Shift to 0-DAC_MASK range
int32_t full_range_value = (int32_t)ideal_deviation + DAC_MIDPOINT;
ideal_value_0_to_max = (uint16_t)dds_clamp(full_range_value, 0, DAC_MASK);
break;
}


R-2R DAC 写入 (高位 GPIO 操作):

void dds_write_dac(uint16_t value) {
value &= DAC_MASK; // Ensure value is within 10 bits (0-1023)
// Create masks and values shifted to the correct GPIO positions (LSB at DAC_LSB_PIN)
uint64_t gpio_mask = (uint64_t)DAC_MASK << DAC_LSB_PIN;
uint64_t gpio_values = (uint64_t)value << DAC_LSB_PIN;
// Use gpio_put_masked64 for atomic update across low and high GPIO banks
gpio_put_masked64(gpio_mask, gpio_values);
}



按键/开关状态解码:

8 位 R–2R 阶梯 DAC,8 个开关就对应二进制码的 8 位。


uint16_t dds_process_input_adc(void) {
// ... (ADC reading and filtering)
adc_difference = abs(ADC_BASELINE - (int)get_filtered_adc());
uint16_t tolerance = 10;
uint8_t current_detected_mask = findPressedKeys(adc_difference, tolerance, keys, num_keys);

return adc_raw; // Return raw ADC value if needed elsewhere
}


高位 GPIO 初始化适配:


对编号超过31的GPIO引脚支持。

rp2350B有很多GPIO,引脚号小于32的GPIO由GPIO_IN 和 GPIO_OUT寄存器控制,大于32的GPIO由GPIO_HI_IN 和 GPIO_HI_OUT寄存器控制,所以要使用gpio_put_masked64函数,操作编号大于32的GPIO口。
除此之外,还要补充pico SDK中批量初始化的函数,增加如下gpio_init_mask64函数:


void gpio_init_mask64(uint64_t mask) {
for (uint i = 0; i < NUM_BANK0_GPIOS; ++i) { // NUM_BANK0_GPIOS should be defined correctly for RP2350B
if (mask & ((uint64_t)1 << i)) {
gpio_init(i);
}
}
}

// In dds_init_gpio, using the custom function
void dds_init_gpio(void) {
uint64_t dac_pin_mask = (uint64_t)DAC_MASK << DAC_LSB_PIN; // e.g., (0x3FF) << 28
gpio_init_mask64(dac_pin_mask); // Initialize GP28-GP37
gpio_set_dir_out_masked64(dac_pin_mask); // Set direction using 64-bit mask function
}



实物演示

信号的模拟输出通过一个 R-2R 梯形电阻网络 实现,构成一个简易的数模转换器(DAC)。DAC 的输出经过电压跟随器进行缓冲,以隔离负载影响。本项目中,R-2R 网络连接到 RP2350B 的 GP28 至 GP37 引脚(共 10 位)。 输入部分包括:

  1. 拨码开关组和按键组:通过一个电阻分压网络连接到 RP2350B 的一个 ADC 引脚(GP27/ADC1)。通过读取不同的 ADC 电压值来识别不同的开关和按键组合状态,用于切换波形和调整频率。
  2. 电位计:连接到 ADS7868 ADC,用于模拟输入信号幅度的调节。ADS7868 通过 PIO 实现的 SPI 接口与 RP2350B 通信(GP14-SDO, GP15-SCLK, GP16-CS)。 显示部分采用一个 OLED 显示屏,通过软件模拟 SPI 方式连接到 RP2350B 的高位 GPIO 引脚(GP41-CS, GP42-DC, GP43-RESET, GP44-DIN, GP45-CLK)。

本项目所生成的正弦波、三角波、方波、直流的实际波形如下图所示:


具体可见演示视频

总结

本项目依托硬禾RP2350B核心板和综合训练平台,依赖其强大的处理能力和独特的 PIO 可编程 I/O 模块,成功实现了一个功能全面的 DDS 信号发生器。通过结合 DDS 技术、R-2R DAC、PIO 外设模拟、软件模拟 SPI、以及 ADC 复合输入解码方案,克服了硬件接口和操作高位 GPIO 的限制

通过本次项目实践,学习了如何利用 ADC 采样结合电阻网络,仅通过一个 GPIO 引脚即可有效获取多达 8 个按键或开关的状态信息。同时,项目还解决了 RP2350B 高位 GPIO 的操作问题,让我们深入理解了其新增的 GPIO 控制寄存器机制(如 GPIO_HI_OUT 等),并通过对底层 SDK 配置文件的必要修改,成功实现了对编号大于 31 的 IO 口的精确操控系统能够稳定生成正弦波、三角波、方波和直流信号,频率和幅度在指定范围内可调,并通过 OLED 屏实时显示状态,达到了预期的设计目标。


附件下载
rp2350B_dds.zip
在C SDK的board文件夹中复制pico.h,重命名为eetree_rp2350b.h文件,将#define PICO_RP2350A 1改为#define PICO_RP2350A 0
团队介绍
团队成员
TetraPak
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号