基于RP2350B实现DDS信号发生器
该项目使用了RP2350B和综合技能训练板,实现了可调波形、频率、幅度、直流偏移的DDS信号发生器的设计,它的主要功能为:通过DAC输出波形、在显示屏上显示波形、在显示屏上显示波形幅度频率等参数。 该项目使用了RP2350B核心板和circuitpython语言,实现了DDS信号发生器的设计,它的主要功能为:根据按键、拨码开关、电位计状态通过DAC输出特定的频率、幅度、波形,并通过OLED显示这些信息。
标签
测试
DDS
ADC
开发板
derrickcr
更新2025-07-10
北京邮电大学
43

项目总结报告

一、    项目介绍

本项目为硬禾学堂设计的任务3:利用DAC制作一个可调波形、频率、幅度的DDS信号发生器。具体任务要求如下:

1.能够产生正弦波、三角波、方波、直流,可以通过核心板的拨码开关控制波形的切换。

2.产生信号的幅度0-3Vpp之间可调,可以通过电位计进行调节。

3.产生信号的频率100Hz - 200KHz之间可调。

4.产生的波形、波形的幅度、波形的频率都实时显示在OLED屏幕上。

二、    硬件介绍

本项目使用了硬核学堂提供的RP2350B核心板和综合技能训练板。

2.1 RP2350B核心板

硬禾学堂推出的RP2350B核心板在芯片性能方面表现出色。其搭载的双核处理器,包括双核 Arm Cortex-M33 或双核 Hazard3 RISC-V 处理器,频率高达150MHz,可实现高效的数据处理和任务调度。此外,该核心板还配备了520KB片上SRAM4MB板载闪存, 为程序运行和数据存储提供了充足的空间,能够支持更复杂的算法和功能。

该核心板接口丰富且兼容性强,兼容小脚丫FPGARP2350B拥有48IO,相较于RP2040RP2350A30IO,增加了18IO。这一增加的IO数量可以通过合理配置访问更多外设。该核心板还提供了SPII2CUARTADCPWMDAC等多种接口,可满足不同外设的连接需求,实现与传感器、显示器、通信模块等设备的连接。

2.2 综合技能训练板

基于小脚丫FPGA的综合技能训练平台是由苏州思得普科技有限公司开发的一款功能强大的学习工具,旨在帮助用户,尤其是学生和工程师,通过实践项目深入理解数字电子技术和FPGA开发的关键概念。

该训练板功能丰富,有任意波形/信号发生器的功能、ADC数据采集的功能、传感器信息输入功能、OLED图形化信息显示功能、蜂鸣器输出功能、UART通信功能,为学生提供了丰富的学习训练资源。本项目使用该训练板完成。


三、    方案框图和项目设计思路

1 方案框图

本项目旨在设计一个DDS信号发生器。其核心是利用 RP2350B 核心板实现多种波形的产生与调节。

在硬件方面,首先,通过电位计调节信号幅度,电位计输出的模拟量经由 ADS7868采样后,再利用SPI总线传输至RP2350BRP2350B 根据接收到的电位计数据控制信号幅度,使其在0–3.3Vpp 之间可调。同时,按键网络经ADC转换后输入到 RP2350B,实现对核心板拨码开关状态的检测,进而控制波形的切换,选择生成正弦波、三角波、方波和直流。

对于波形的产生,使用10bit IO输出至R-2R网络,将数字信号转换为模拟信号,从而产生所需的任意波形。在频率调节方面,通过按键调整信号频率,使产生信号的频率可在100Hz-200KHz之间调节。


四、    软件流程图和关键代码介绍

4.1 软件流程图

2 软件流程图

4.2 关键代码

本项目使用circuitpython完成,其串行运行逻辑从软件流程图可知,较为简单,便不再解释代码。下面分别介绍不同模块的驱动程序。

4.2.1 DAC驱动

该段代码首先初始化了DA0DA9共十个数字输入输出引脚,并将它们的方向设置为输出。接着定义了一个get_DAC_data函数,根据输入的命令(cmd)来返回不同类型波形的数据列表。如果是命令 “0”,返回正弦波数据;命令“1”返回三角波数据;命令“2”返回方波数据;命令“3”返回直流信号数据;其他命令则返回一个包含0的列表。

DA0 = digitalio.DigitalInOut(board.GP28)
DA1 = digitalio.DigitalInOut(board.GP29)
DA2 = digitalio.DigitalInOut(board.GP30)
DA3 = digitalio.DigitalInOut(board.GP31)
DA4 = digitalio.DigitalInOut(board.GP32)
DA5 = digitalio.DigitalInOut(board.GP33)
DA6 = digitalio.DigitalInOut(board.GP34)
DA7 = digitalio.DigitalInOut(board.GP35)
DA8 = digitalio.DigitalInOut(board.GP36)
DA9 = digitalio.DigitalInOut(board.GP37)
DA0.direction = digitalio.Direction.OUTPUT
DA1.direction = digitalio.Direction.OUTPUT
DA2.direction = digitalio.Direction.OUTPUT
DA3.direction = digitalio.Direction.OUTPUT
DA4.direction = digitalio.Direction.OUTPUT
DA5.direction = digitalio.Direction.OUTPUT
DA6.direction = digitalio.Direction.OUTPUT
DA7.direction = digitalio.Direction.OUTPUT
DA8.direction = digitalio.Direction.OUTPUT
DA9.direction = digitalio.Direction.OUTPUT

# data = [511, 669, 811, 924, 997, 1022, 997, 924, 811, 669, 511, 353, 211, 98, 25, 0, 25, 98, 211, 353]

def get_DAC_data(cmd):
    # SINE
    if(cmd == "0"):
        data = [511, 543, 575, 607, 638, 669, 699, 729, 757, 785, 811, 837, 861, 884, 905, 924, 942, 959, 973, 986, 997, 1006, 1013, 1018, 1021, 1022, 1021, 1018, 1013, 1006, 997, 986, 973, 959, 942, 924, 905, 884, 861, 837, 811, 785, 757, 729, 699, 669, 638, 607, 575, 543, 511, 479, 447, 415, 384, 353, 323, 293, 265, 237, 211, 185, 161, 138, 117, 98, 80, 63, 49, 36, 25, 16, 9, 4, 1, 0, 1, 4, 9, 16, 25, 36, 49, 63, 80, 98, 117, 138, 161, 185, 211, 237, 265, 293, 323, 353, 384, 415, 447, 479]
    # TRAINGLE
    elif(cmd == "1"):
        data = [0, 20, 41, 61, 82, 102, 123, 143, 164, 184, 205, 225, 246, 266, 286, 307, 327, 348, 368, 389, 409, 430, 450, 471, 491, 512, 532, 552, 573, 593, 614, 634, 655, 675, 696, 716, 737, 757, 777, 798, 818, 839, 859, 880, 900, 921, 941, 962, 982, 1003, 1023, 1003, 982, 962, 941, 921, 900, 880, 859, 839, 818, 798, 777, 757, 737, 716, 696, 675, 655, 634, 614, 593, 573, 552, 532, 512, 491, 471, 450, 430, 409, 389, 368, 348, 327, 307, 286, 266, 246, 225, 205, 184, 164, 143, 123, 102, 82, 61, 41, 20]
    # SQUARE
    elif(cmd == "2"):
        data = [1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    # DC
    elif(cmd == "3"):
        data = [1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023,1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023, 1023]
    else:
        data = [0]
    return data

 

4.2.2 读电位计

该段代码用于读取电位计数值。它首先设置了三个 GPIO 引脚:数据输出(DCP_SDO)、时钟(DCP_SCLK)和片选(DCP_CS_N)。在read_dcp函数中,通过控制这些引脚的高低电平来同步与电表的通信。该函数首先发送4个时钟周期的信号以启动数据读取,然后通过8个时钟周期依次读取电表输出的8位数据位。读取到的数据经过二进制移位和组合,最终将8位数据转换为03.3V的电压值并返回。

DCP_SDO = digitalio.DigitalInOut(board.GP14)
DCP_SCLK = digitalio.DigitalInOut(board.GP15)
DCP_CS_N = digitalio.DigitalInOut(board.GP16)
DCP_SDO.direction = digitalio.Direction.INPUT
DCP_SCLK.direction = digitalio.Direction.OUTPUT
DCP_CS_N.direction = digitalio.Direction.OUTPUT
def read_dcp():
    t = 1/(1e7)
    DCP_CS_N.value = 1
    DCP_SCLK.value = 1
    time.sleep(t/2)
    data = 0b0000_0000
    for i in range(4):
        DCP_CS_N.value = 0
        DCP_SCLK.value = 1
        time.sleep(t)
        DCP_SCLK.value = 0
        time.sleep(t)
    for i in range(8):
        DCP_CS_N.value = 0
        DCP_SCLK.value = 0
        time.sleep(t)
        DCP_SCLK.value = 1
        data_bit = DCP_SDO.value
        data = (data << 1) | data_bit
        time.sleep(t)
    DCP_CS_N.value = 1
    DCP_SCLK.value = 1
    data = data/255*3.3
    return data

   

4.2.3 生成字体

由于没有找到合适的字体读取、OLED驱动库,因此自己通过逐个点亮LED的方式设计了各种输出的各种字体。字体字典内容如下。

num_fonts = {
    "0":[[1,2], [1,3], [1,4], [2,1], [2,5], [3,1], [3,5], [4,1], [4,5], [5,1], [5,5], [6,1], [6,5], [7,2], [7,3], [7,4]],
    "1":[[1,4], [2,3], [2,4], [3,4], [4,4], [5,4], [6,4], [7,4]],
    "2":[[1,2], [1,3], [1,4], [2,1], [2,5], [3,1], [3,5], [4,5], [5,4], [6,3], [7,1], [7,2], [7,3], [7,4], [7,5]],
    "3":[[1,2], [1,3], [1,4], [2,1], [2,5], [3,1], [4,3], [4,4], [5,5], [6,1], [6,5], [7,2], [7,3], [7,4]],
    "4":[[1,4], [2,3], [2,4], [3,2], [3,4], [4,1], [4,2], [4,3], [4,4], [4,5], [5,4], [6,4], [7,4]],
    "5":[[1,2], [1,3], [1,4], [1,5], [2,2], [3,2], [3,3], [3,4], [4,5], [5,5], [6,5], [7,2], [7,3], [7,4]],
    "6":[[1,3], [2,2], [3,2], [3,3], [3,4], [4,1], [4,5], [5,1], [5,5], [6,1], [6,5], [7,2], [7,3], [7,4]],
    "7":[[1,2], [1,3], [1,4], [1,5], [2,5], [3,5], [4,5], [5,4], [6,4], [7,4]],
    "8":[[1,2], [1,3], [1,4], [2,1], [2,5], [3,1], [3,5], [4,2], [4,3], [4,4], [5,1], [5,5], [6,1], [6,5], [7,2], [7,3], [7,4]],
    "9":[[1,2], [1,3], [1,4], [2,1], [2,5], [3,1], [3,5], [4,1], [4,5], [5,2], [5,3], [5,4], [6,3], [7,2]],
    "=":[[3,2], [3,3], [3,4], [5,2], [5,3], [5,4]],
    "F":[[1,2], [1,3], [1,4], [1,5], [2,2], [3,2], [4,2], [4,3], [4,4], [5,2], [6,2], [7,2]],
    "k":[[1,2], [2,2], [3,2], [3,4], [4,2], [4,3], [5,2], [6,2], [6,3], [7,2], [7,4]],
    "H":[[1,2], [1,5], [2,2], [2,5], [3,2], [3,5], [4,2], [4,3], [4,4], [4,5], [5,2], [5,5], [6,2], [6,5], [7,2], [7,5]],
    "z":[[4,2], [4,3], [4,4], [4,5], [5,4], [6,3], [7,2], [7,3], [7,4], [7,5]],
    "A":[[1,3], [2,2], [2,4], [3,2], [3,4], [4,2], [4,3], [4,4], [5,1], [5,5], [6,1], [6,5], [7,1], [7,5]],
    "V":[[1,1], [1,5], [2,1], [2,5], [3,1], [3,5], [4,2], [4,4], [5,2], [5,4], [6,2], [6,4], [7,3]],
    ".":[[6,3], [6,4], [7,3], [7,4]],
    "help":"汉字的尺寸为7×5"
    }

wave_fonts = {
    "1":[[0, 8], [1, 9], [2, 11], [3, 13], [4, 14], [5, 15], [6, 15], [7, 15], [8, 15], [9, 14], [10, 12], [11, 11], [12, 9], [13, 8], [14, 6], [15, 4], [16, 2], [17, 1], [18, 1], [19, 1], [20, 1], [21, 2], [22, 3], [23, 4], [24, 6], [25, 8], [26, 9], [27, 11], [28, 13], [29, 14], [30, 15], [31, 15], [32, 15], [33, 15], [34, 14], [35, 12], [36, 11], [37, 9], [38, 8], [39, 6]],
    "2":[[0, 8], [1, 8], [2, 7], [3, 6], [4, 5], [5, 4], [6, 4], [7, 3], [8, 2], [9, 1], [10, 0], [11, 1], [12, 2], [13, 3], [14, 4], [15, 4], [16, 5], [17, 6], [18, 7], [19, 8], [20, 8], [21, 8], [22, 9], [23, 10], [24, 11], [25, 12], [26, 12], [27, 13], [28, 14], [29, 15], [30, 16], [31, 15], [32, 14], [33, 13], [34, 12], [35, 12], [36, 11], [37, 10], [38, 9], [39, 8]],
    "3":[[0, 0], [1, 0], [2, 0], [3, 0], [4, 0], [5, 0], [6, 0], [7, 0], [8, 0], [9, 0], [10, 0], [11, 0], [12, 0], [13, 0], [14, 0], [15, 0], [16, 0], [17, 0], [18, 0], [19, 0], [20, 16], [21, 16], [22, 16], [23, 16], [24, 16], [25, 16], [26, 16], [27, 16], [28, 16], [29, 16], [30, 16], [31, 16], [32, 16], [33, 16], [34, 16], [35, 16], [36, 16], [37, 16], [38, 16], [39, 16], [20,16], [20,15], [20,14], [20,13], [20,12], [20,11], [20,10], [20,9], [20,8], [20,7], [20,6], [20,5], [20,4], [20,3], [20,2], [20,1], [20,0]],
    "4":[[0, 3], [1, 3], [2, 3], [3, 3], [4, 3], [5, 3], [6, 3], [7, 3], [8, 3], [9, 3], [10, 3], [11, 3], [12, 3], [13, 3], [14, 3], [15, 3], [16, 3], [17, 3], [18, 3], [19, 3], [20, 3], [21, 3], [22, 3], [23, 3], [24, 3], [25, 3], [26, 3], [27, 3], [28, 3], [29, 3], [30, 3], [31, 3], [32, 3], [33, 3], [34, 3], [35, 3], [36, 3], [37, 3], [38, 3], [39, 3]],
    "help":"顺序为sine/traignle/square/DC"
    }

 

    4.2.4 OLED驱动

在编程时发现很多现成的OLED字体驱动程序都无法使用,因此这里使用逐个点亮LED的方式显示字和图像。由于开发板上的SPI总线无法使用,因此将低位的SPI使用杜邦线连接到相应接口,并使用ssd1306的驱动库进行驱动。驱动程序如下。该程序首先将需要修改的部分清空,然后再逐个点亮LED显示需要显示的内容。

初始化代码如下:

OLED_CLK = board.GP18
OLED_MOSI = board.GP19
useless1 = digitalio.DigitalInOut(board.GP44)
useless1.direction = digitalio.Direction.INPUT
useless2 = digitalio.DigitalInOut(board.GP45)
useless2.direction = digitalio.Direction.INPUT
'''
CS_N 41
DC 42
RESET 43
DIN 44
CLK 45
'''
OLED_CS_N = digitalio.DigitalInOut(board.GP41)
OLED_DC = digitalio.DigitalInOut(board.GP42)
OLED_RESET = digitalio.DigitalInOut(board.GP43)
spi = busio.SPI(clock=OLED_CLK, MOSI=OLED_MOSI)
oled = SSD1306_SPI(128, 32, spi, OLED_DC, OLED_RESET, OLED_CS_N)
oled.fill(0)


驱动输出:

oled.fill_rect(voltage_pos_x, voltage_pos_y, voltage_pos_x+5*8, voltage_pos_y+8, 0)
for x in range(len(voltage_show)):
voltage_show_char = voltage_show[x]
voltage_show_font = num_fonts[voltage_show_char]
for i,j in voltage_show_font:
oled.pixel(j+voltage_pos_x+int(5*x), i+voltage_pos_y, 1)


4.2.5 读取按键和拨码开关

该段代码实现了按键、拨码开关检测与选择功能。首先初始化工作指示灯(work_led)和模拟输入引脚(ADC)。接着定义了不同按键对应的电压值。choose_sw函数根据ADC读取到的电压值,通过比较不同按键电压的中间值来判断哪个按键被按下,并返回对应的按键编号,从而实现按键的检测与选择功能。

work_led = digitalio.DigitalInOut(board.GP3)
work_led.switch_to_output(value=False, drive_mode=digitalio.DriveMode.PUSH_PULL)
ADC = analogio.AnalogIn(board.GP47)
all_fall = 3.028
sw4_value = 2.94021
sw3_value = 2.98684
sw2_value = 3.01074
sw1_value = 3.02388
def choose_sw(ADC_value):
middle_01 = 3.032
middle_12 = (sw1_value + sw2_value)/2
middle_23 = (sw2_value + sw3_value)/2
middle_34 = (sw3_value + sw4_value)/2
button_middle = 2
if (ADC_value > middle_01):
return 0
elif (ADC_value > middle_12):
return 1
elif (ADC_value > middle_23):
return 2
elif (ADC_value > middle_34):
return 3
elif (ADC_value > 2.86):
return 4
elif (ADC_value >button_middle):
# 最左边按键
return 5
else:
# 最右边按键
return 6

 

五、    实物功能演示图及说明

输出正弦波,可以看到显示屏上画出了正弦波的波形以及Vppfreq的数值,示波器也显示了正弦波的波形:

3 演示图1 

 

输出三角波:

4 演示图2

输出方波:

5 演示图3

输出直流:

6 演示图4

增加频率(从左图40kHz到右图152kHz):

 

7 演示图5

减小频率(从左图166kHz到右图26kHz):

8 演示图6

调节Vpp,下面三张图的顺序为扭电位计、示波器输出峰值被截断、按示波器auto键重新调整输出:

9演示图7 

输出三角波时也有相同功能:

10演示图8


六、    项目中遇到的难题和解决办法

1.软件平台不易搭建。在树莓派Pico常用的micropython环境下,其官方只给了基于RP2350A的固件,只能使用低30位的引脚,操作不便,于是才选择使用circuitpython

2. 连接OLED部分的硬件SPI无法使用。模拟SPI又比较繁琐,因此将可以使用的硬件SPI拉过来实现OLED的驱动。


七、    对本次活动的心得体会

作为一个大三通信工程的本科生,我在原先的学习过程中并没有看过很多的电路原理图,因此最初做起来还是比较吃力的。不过在读懂图、完成驱动后逻辑还是比较简单的。总的来说多学点东西还是挺有意思的。

 

 

附件下载
work.zip
工程代码
团队介绍
北京邮电大学信息与通信工程学院2022级本科生
团队成员
derrickcr
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号