2021暑假一起练-基于树莓派PICO带有背景音乐的数码相框
基于树莓派PICO和电子森林的扩展板,实现了一个带有背景音乐的数码相框。主要完成蜂鸣器播放音乐、姿态传感器数据读取、TF卡文件读取、LCD显示屏现实图片和温度的读取与显示等。
标签
树莓派
嵌入式系统
PICO
数码相框
student
更新2021-09-09
1365

基于树莓派PICO和电子森林的扩展板,实现了一个带有背景音乐的数码相框。主要完成蜂鸣器播放音乐、姿态传感器数据读取、TF卡文件读取、LCD显示屏现实图片和温度的读取与显示等。

实现了以下功能:

  1. 将三张照片保存在SD卡中,能够在240*240的LCD屏幕上以4种不同的切换模式轮流播放照片,模式的切换由按键控制
  2. 播放照片的同时,播放背景音乐,通过蜂鸣器输出
  3. 利用姿态传感器,旋转板卡,照片可以自动旋转,保证无论板卡是什么方向,照片的方向都是正的

作品功能框图

ForQL-HwN9Ee_xg5LQc8DvqDyedM

  • 两个按键用来控制切换图片和设置切换模式
  • 编码器按键用来控制蜂鸣器音乐的播放与暂停
  • tf卡保存图片bin文件
  • 通过ADC读取PICO内置温度传感器
  • 通过PWM驱动蜂鸣器播放音乐
  • 通过I2C驱动姿态传感器,读取姿态
  • 通过SPI驱动240*240的LCD屏显示图片

作品的代码编写 1. 按键部分

按键由两个微动开关和一个编码器按键组成。采用中断的触发方式,按键1控制图片的切换,按键2设置图片切换模式,编码器按键控制蜂鸣器的暂停与恢复。

按键中断的优先级高于定时器中断,原本置于按键中断的图片刷新放到了主循环中,按键仅修改参数,产生相关的标志位。

按键中断采用了延时软件消抖的方法,微动开关延时10ms,而编码器按键由于其特殊的物理结构,需要延时100ms,所以设置其任务为不常使用的暂停与恢复蜂鸣器播放的功能,这也避免了外部中断对定时器中断的影响。

# 按键中断,用于切换图片
def key1callback(arg):
    global loop_i
    global change
    
    if key1.value() == 0:
        utime.sleep(0.01)
        if key1.value() == 0:
            led.toggle()
            loop_i = loop_i+1
            if loop_i>=3:
                loop_i = 0
            change = 1
#             rf.ReadFile(display, filenames[loop_i], rf_mode)
#             display.text(font1, str(round(thermometer(),1)), 208, 232, color=st7789.WHITE, background=st7789.BLACK)

# 按键中断,用于设置切换图片模式
def key2callback(arg):
    global rf_mode
    
    if key2.value() == 0:
        utime.sleep(0.01)
        if key2.value() == 0:
            led.toggle()
            rf_mode += 1
            if rf_mode >= 4:
                rf_mode = 0

# 按键中断,用于控制蜂鸣器的播放与暂停
def key_pre_callback(arg):
    if key_pre.value() == 0:
        utime.sleep(0.1)
        if key_pre.value() == 0:
            if mySong.stopped:
                mySong.continue_()
            else:
                mySong.stop()


# 按键、LED初始化---------------------------------------------------------------------------------
key_pre = machine.Pin(6, machine.Pin.IN)
key1 = machine.Pin(7, machine.Pin.IN)
key2 = machine.Pin(8, machine.Pin.IN)
key_pre.irq(trigger=machine.Pin.IRQ_FALLING , handler=key_pre_callback)
key1.irq(trigger=machine.Pin.IRQ_FALLING , handler=key1callback)
key2.irq(trigger=machine.Pin.IRQ_FALLING , handler=key2callback)

led = machine.Pin(25, machine.Pin.OUT, value=True)

2.LCD的显示

LCD显示部分主要使用的是一个st7789的库,文件有774行,受篇幅限制,就不放在正文里了。文件来源https://www.eetree.cn/project/detail/234

表示衷心的感谢。但是其中有一个函数,我对其有一定的修改,有必要单独列出来。

我给blit_buffer函数多加了一个参数,为了能够给我后续的图片刷新带来更多的便利。

def blit_buffer(self, buffer, x, y, width, height, index=None):
        """
        Copy buffer to display at the given location.

        Args:
            buffer (bytes): Data to copy to display
            x (int): Top left corner x coordinate
            Y (int): Top left corner y coordinate
            width (int): Width
            height (int): Height
        """
        if index is None:
            self.set_window(x, y, x + width - 1, y + height - 1)
            self.write(None, buffer)
        elif index == 0:
            self.set_window(x, y, x + width - 1, y + height - 1)
            self.write(None, buffer)
        else:
            self.write(None, buffer)

# LCD初始化---------------------------------------------------------------------------------------
st7789_res = 0
st7789_dc  = 1
disp_width = 240
disp_height = 240
spi_sck=machine.Pin(2)
spi_tx=machine.Pin(3)
spi0=machine.SPI(0,baudrate=4000000, phase=0, polarity=1, sck=spi_sck, mosi=spi_tx)
display = st7789.ST7789(spi0, disp_width, disp_width,
                          reset=machine.Pin(st7789_res, machine.Pin.OUT),
                          dc=machine.Pin(st7789_dc, machine.Pin.OUT),
                          xstart=0, ystart=0, rotation=0)
display.fill(st7789.BLUE)

在显示图片部分,我参考了fill_rect函数。由于没有仔细阅读st7789的数据手册,我对buffer写入的大小不太了解。fill_rect函数与图片显示函数十分相似,不同点在于一个像素颜色是完全相同的,一个是不同的。fill_rect函数一次write的大小是512byte,最终我测试了512,256,128,1024四种大小,前三种的速度在以秒为单位的计时方式下时间完全一致,最后一种会报错。

def fill_rect(self, x, y, width, height, color):
        """
        Draw a rectangle at the given location, size and filled with color.

        Args:
            x (int): Top left corner x coordinate
            y (int): Top left corner y coordinate
            width (int): Width in pixels
            height (int): Height in pixels
            color (int): 565 encoded color
        """
        self.set_window(x, y, x + width - 1, y + height - 1)
        chunks, rest = divmod(width * height, _BUFFER_SIZE)
        pixel = _encode_pixel(color)
        self.dc.on()
        if chunks:
            data = pixel * _BUFFER_SIZE
#             print(type(data), len(data))
            for _ in range(chunks):
                self.write(None, data)
        if rest:
            self.write(None, pixel * rest)

3.tf卡的文件读取与图片的显示模式

tf卡的读取是我开始作品设计的第一个部分,也是给我当头棒喝的一个部分。我手里的1张128g,1张64g,3张32g,1张16g,1张2g的tf卡全部无法读取。虽然是硬件设计的问题,但这给我带来了很大的挫败感,也导致从一开始的激情满满到痛不欲生。于是作品被丢到一边,一直到项目结束的时候才被重新捡起来。感谢刘锐卿在调试过程中给予的帮助。

最后工作的是一张4g的tf卡。

将图片的尺寸修改为240*240的大小,使用https://lvgl.io/tools/imageconverter

将图片转换成bin格式,format分别为True Color和Binary RGB565 Swap。将bin文件通过读卡器拷贝到tf卡中。文件大小为240*240*2+5=115205。最后5个字节不知道是什么,我会选择在读取文件的时候把它们丢弃掉。

import os

_BUFFER_SIZE = const(512)

def ReadFile(display, filepath=None, mode=0):
    global buf
    if filepath is None:
        filepath='1.bin'
    binfile = open(filepath, 'rb') #打开二进制文件
    size = os.stat(filepath)[6] - 5 # 去掉最后5个无用的字节
    print(size)
    
    if mode == 0:
        chunks, rest = divmod(size, _BUFFER_SIZE)
        if chunks:
            for i in range(chunks):
                data = binfile.read(_BUFFER_SIZE) #每次输出512字节
                display.blit_buffer(data, 0, 0, 240, 240, i)
        if rest:
            data = binfile.read(rest) #每次输出一个字节
            display.blit_buffer(data, 0, 0, 240, 240, i+1)
            
    elif mode == 1:
        binfile.seek(76800)
        for i in range(80):
            data = binfile.read(480) 
            display.blit_buffer(data, 0, 160, 240, 80, i)            
        binfile.seek(38400)    
        for i in range(80):
            data = binfile.read(480) #每次输出512字节
            display.blit_buffer(data, 0, 80, 240, 80, i)            
        binfile.seek(0)    
        for i in range(80):        
            data = binfile.read(480) #每次输出512字节
            display.blit_buffer(data, 0, 0, 240, 80, i)
    
    elif mode == 2:
        for i in range(240):
            binfile.seek(115200-480*(i+1))
            data = binfile.read(480) 
            display.blit_buffer(data, 0, 240-(i+1), 240, 1, 0)
            
    elif mode == 3:
        for i in range(240):
            binfile.seek(480*i)
            data = binfile.read(160) 
            display.blit_buffer(data, 0, 0, 80, 240, i)            
        for i in range(240):
            binfile.seek(480*i+160)
            data = binfile.read(160) 
            display.blit_buffer(data, 80, 0, 80, 240, i)            
        for i in range(240):
            binfile.seek(480*i+320)
            data = binfile.read(160) 
            display.blit_buffer(data, 160, 0, 80, 240, i)
    else:
        print("ReadFile wrong!")

    binfile.close()
    
    
# tf卡初始化---------------------------------------------------------------------------------------
sd_spi=machine.SoftSPI(0, sck=machine.Pin(17,machine.Pin.OUT),
                       mosi=machine.Pin(18,machine.Pin.OUT),
                       miso=machine.Pin(19,machine.Pin.IN))
sd=sdcard.SDCard(sd_spi,cs=machine.Pin(22))
sd.init_spi(40_000_000)
sd.init_card()
x=os.mount(sd, '/sd')
os.chdir('sd')
print(os.getcwd())

ReadFile的最后一个参数用来选择图片显示模式。在最基本的正常显示逻辑下,我又设计了3种图片显示模式,本来想设计更多的模式,可是看了一下越来越长的刷新时间,便打消了念头。目前的四种模式为从上往下刷,从下往上刷,横向3段式刷新,纵向三段式刷新。其实现有的刷新模式频率可以通过优化代码来提高,但迫于时间压力,没有继续。

4.温度传感器

树莓派PICO内部集成了温度传感器,只需要读取相应管脚的ADC数值,并经过一定的计算就可以得到。这里我测量了5次并取平均。

import machine
import time

def thermometer():
    temp_a = 0
    sensorTemp = machine.ADC(4)
    offset = 3.3/(65535)
    time.sleep(0.01)
    
    for i in range(5):
        read = sensorTemp.read_u16() * offset
        temp = 27 - (read - 0.706) / 0.001721
        temp_a += temp
    return temp_a/5.0

if __name__ == "__main__":
    a = thermometer()
    print(a, type(a), round(a, 1), type(round(a, 1)))

 

温度数值在LCD屏的右下角显示,并且随着LCD屏的刷新而刷新。

5.蜂鸣器

蜂鸣器使用了buzzer_music文件,可以在https://onlinesequencer.net/

网站导入midi文件,设计音乐。自己尝试了一番,设计的总是类似于小灵通的铃声,很急促而且还不好听。最终选取的是想成为大佬的咸鱼

设计的音乐文件,buzzer_music文件也是拷贝的他的,其它的很多代码也有参考他的,他的项目是我PICO的入门指南,在此一并表示感谢。

蜂鸣器使用了25Hz的定时器进行驱动,避免对图片刷新产生影响。

song = '0 A#4 1 1;2 F5 1 1;4 D#5 1 1;8 D5 1 1;11 D5 1 1;6 A#4 1 1;14 D#5 1 1;'\
        '18 A#4 1 1;20 D#5 1 1;22 A#4 1 1;24 D5 1 1;27 D5 1 1;30 D#5 1 1;32 A#4 1 1;'\
        '34 F5 1 1;36 D#5 1 1;38 A#4 1 1;40 D5 1 1;43 D5 1 1;46 D#5 1 1;50 A#4 1 1;'\
        '52 D#5 1 1;54 G5 1 1;56 F5 1 1;59 D#5 1 1;62 F5 1 1;64 A#4 1 1;66 F5 1 1;'\
        '68 D#5 1 1;70 A#4 1 1;72 D5 1 1;75 D5 1 1;78 D#5 1 1;82 A#4 1 1;84 D#5 1 1;'\
        '86 A#4 1 1;88 D5 1 1;91 D5 1 1;94 D#5 1 1;96 A#4 1 1;100 D#5 1 1;102 A#4 1 1;'\
        '104 D5 1 1;107 D5 1 1;110 D#5 1 1;114 A#4 1 1;116 D#5 1 1;118 G5 1 1;120 F5 1 1;123 D#5 1 1;126 F5 1 1;98 F5 1 1'

# 40ms定时器中断,用于蜂鸣器播放音乐
def tim1callback(arg):
    mySong.tick()

# 按键中断,用于控制蜂鸣器的播放与暂停
def key_pre_callback(arg):
    if key_pre.value() == 0:
        utime.sleep(0.1)
        if key_pre.value() == 0:
            if mySong.stopped:
                mySong.continue_()
            else:
                mySong.stop()

mySong = music(song)
tim1 = machine.Timer() # 初始化定时器
tim1.init(freq=25, mode=machine.Timer.PERIODIC, callback=tim1callback)

6.姿态传感器

姿态传感器的型号为MMA7660,提供了姿态的接口,直接读取即可。

import machine
import struct
import time

class MMA7660:
    sclpin=machine.Pin(11)
    sdapin=machine.Pin(10)
    
    def __init__(self):

        self.i2c=machine.I2C(1, scl=MMA7660.sclpin,sda=MMA7660.sdapin,freq=400000)
        print('MMA7660 address={}'.format(self.i2c.scan()[0]))
        # 使能
        self.i2c.writeto_mem(76,0x07,b'1')
        print('MMA7660 init')
        
    def close(self):
        self.i2c.writeto_mem(76,0x07,b'0')
        print('MMA7660 closed')
        
    # 6bit结果输出,移位操作    
    def bit2num(self,x):
        if x&64:
            return None
        xsign=(x&32)>>5  # 取符号位
        x=x&31     # 取数值
        if xsign==1:
            x=-x
        return x
        
    def readxyz(self):
        x= mma7660.readfrom_mem(76, 0x00, 8) 
        y= mma7660.readfrom_mem(76, 0x01, 8) 
        z= mma7660.readfrom_mem(76, 0x02, 8)
        
        # 数据转换
        x=struct.unpack('<B',x)[0]
        y=struct.unpack('<B',y)[0]
        z=struct.unpack('<B',z)[0]
        
        # 调用函数,6位转为数字
        x=self.bit2num(x)
        y=self.bit2num(y)
        z=self.bit2num(z)

        return x,y,z
    
    def readtilt(self):
        t=self.i2c.readfrom_mem(76, 0x03, 8, addrsize=8)
        t1 = struct.unpack('<8B',t)[0]
        t2 = (t1<<3)>>5
        if not t2 ^ 0b110:
            pose = 'up'
        elif not t2 ^ 0b010:
            pose = 'ri'
        elif not t2 ^ 0b101:
            pose = 'do'
        elif not t2 ^ 0b001:
            pose = 'le'
        else:
            pose = None
        return pose


if __name__ == "__main__":
    a = MMA7660()
    while True:
        print(a.readtilt())
        time.sleep(0.1)

代码主体由刘锐卿编写,表示感谢。

7.图片显示方向

图片显示方向由姿态传感器的返回结果决定,当姿态发生变化时,更改图片显示方向并刷新图片。在显示新的图片以前,需要先将显示刷成纯色背景,这样图片的刷新才有较好的效果。

# 更新姿态并刷新图片
def update_pose():
    global pre_pose
    global change
    
    pose = mma.readtilt()
    print(pose)
    if change == 1:
        change = 0
        pass
    elif pre_pose == pose or pose== None :
        pre_pose = pose
        return
    elif pose=='up':
        display.rotation(0)
        display.xstart = 0
        display.ystart = 0
    elif pose=='le':
        display.rotation(1)
        display.xstart = 0
        display.ystart = 0
    elif pose=='do':
        display.rotation(2)
        display.xstart = 0
        display.ystart = 80
    elif pose=='ri':
        display.rotation(3)
        display.xstart = 80
        display.ystart = 0
    else:
        pass
    pre_pose = pose
    display.fill(st7789.BLACK)
    rf.ReadFile(display, filenames[loop_i], rf_mode)
    display.text(font1, str(round(thermometer(),1)), 208, 232, color=st7789.WHITE, background=st7789.BLACK)

在图片不同方向显示上,虽然st7789.py文件说明是240*240的显示屏驱动,但实际使用有一定的bug,体现在rotation()函数在参数是1和2的时候,LCD显示屏只刷新一部分。猜测是驱动是为240*320的屏幕写的。参考https://www.eetree.cn/project/detail/513

修改起始点,完美解决问题。对作者表示感谢。

8.程序逻辑

整个程序的大部分功能在中断中完成,只有图片刷新这样的耗时功能在主循环中完成,这样可以保证所有功能的正常并行运行。

import machine
import os
import utime
from drive import sdcard
from drive import st7789 
from drive import ReadFile as rf
from drive.MMA7660 import MMA7660 
from drive.buzzer_music import music
from drive.thermometer import thermometer
from fonts import vga2_8x8 as font1
from fonts import vga1_16x32 as font2

rf_mode = 0 # 图片刷新模式
pre_pose = None  # 记录上一个姿态
change = 0 # 按键切换图片标志

# 40ms定时器中断,用于蜂鸣器播放音乐
def tim1callback(arg):
    mySong.tick()

# 按键中断,用于切换图片
def key1callback(arg):
    global loop_i
    global change
    
    if key1.value() == 0:
        utime.sleep(0.01)
        if key1.value() == 0:
            led.toggle()
            loop_i = loop_i+1
            if loop_i>=3:
                loop_i = 0
            change = 1
#             rf.ReadFile(display, filenames[loop_i], rf_mode)
#             display.text(font1, str(round(thermometer(),1)), 208, 232, color=st7789.WHITE, background=st7789.BLACK)

# 按键中断,用于设置切换图片模式
def key2callback(arg):
    global rf_mode
    
    if key2.value() == 0:
        utime.sleep(0.01)
        if key2.value() == 0:
            led.toggle()
            rf_mode += 1
            if rf_mode >= 4:
                rf_mode = 0

# 按键中断,用于控制蜂鸣器的播放与暂停
def key_pre_callback(arg):
    if key_pre.value() == 0:
        utime.sleep(0.1)
        if key_pre.value() == 0:
            if mySong.stopped:
                mySong.continue_()
            else:
                mySong.stop()

# 更新姿态并刷新图片
def update_pose():
    global pre_pose
    global change
    
    pose = mma.readtilt()
    print(pose)
    if change == 1:
        change = 0
        pass
    elif pre_pose == pose or pose== None :
        pre_pose = pose
        return
    elif pose=='up':
        display.rotation(0)
        display.xstart = 0
        display.ystart = 0
    elif pose=='le':
        display.rotation(1)
        display.xstart = 0
        display.ystart = 0
    elif pose=='do':
        display.rotation(2)
        display.xstart = 0
        display.ystart = 80
    elif pose=='ri':
        display.rotation(3)
        display.xstart = 80
        display.ystart = 0
    else:
        pass
    pre_pose = pose
    display.fill(st7789.BLACK)
    rf.ReadFile(display, filenames[loop_i], rf_mode)
    display.text(font1, str(round(thermometer(),1)), 208, 232, color=st7789.WHITE, background=st7789.BLACK)

song = '0 A#4 1 1;2 F5 1 1;4 D#5 1 1;8 D5 1 1;11 D5 1 1;6 A#4 1 1;14 D#5 1 1;'\
        '18 A#4 1 1;20 D#5 1 1;22 A#4 1 1;24 D5 1 1;27 D5 1 1;30 D#5 1 1;32 A#4 1 1;'\
        '34 F5 1 1;36 D#5 1 1;38 A#4 1 1;40 D5 1 1;43 D5 1 1;46 D#5 1 1;50 A#4 1 1;'\
        '52 D#5 1 1;54 G5 1 1;56 F5 1 1;59 D#5 1 1;62 F5 1 1;64 A#4 1 1;66 F5 1 1;'\
        '68 D#5 1 1;70 A#4 1 1;72 D5 1 1;75 D5 1 1;78 D#5 1 1;82 A#4 1 1;84 D#5 1 1;'\
        '86 A#4 1 1;88 D5 1 1;91 D5 1 1;94 D#5 1 1;96 A#4 1 1;100 D#5 1 1;102 A#4 1 1;'\
        '104 D5 1 1;107 D5 1 1;110 D#5 1 1;114 A#4 1 1;116 D#5 1 1;118 G5 1 1;120 F5 1 1;123 D#5 1 1;126 F5 1 1;98 F5 1 1'
filenames = ['pcb.bin', 'yh.bin', 'rp.bin']
loop_i = 0

# 按键、LED初始化---------------------------------------------------------------------------------
key_pre = machine.Pin(6, machine.Pin.IN)
key1 = machine.Pin(7, machine.Pin.IN)
key2 = machine.Pin(8, machine.Pin.IN)
key_pre.irq(trigger=machine.Pin.IRQ_FALLING , handler=key_pre_callback)
key1.irq(trigger=machine.Pin.IRQ_FALLING , handler=key1callback)
key2.irq(trigger=machine.Pin.IRQ_FALLING , handler=key2callback)

led = machine.Pin(25, machine.Pin.OUT, value=True)
# tf卡初始化---------------------------------------------------------------------------------------
sd_spi=machine.SoftSPI(0, sck=machine.Pin(17,machine.Pin.OUT),
                       mosi=machine.Pin(18,machine.Pin.OUT),
                       miso=machine.Pin(19,machine.Pin.IN))
sd=sdcard.SDCard(sd_spi,cs=machine.Pin(22))
sd.init_spi(40_000_000)
sd.init_card()
x=os.mount(sd, '/sd')
os.chdir('sd')
print(os.getcwd())

# LCD初始化---------------------------------------------------------------------------------------
st7789_res = 0
st7789_dc  = 1
disp_width = 240
disp_height = 240
spi_sck=machine.Pin(2)
spi_tx=machine.Pin(3)
spi0=machine.SPI(0,baudrate=4000000, phase=0, polarity=1, sck=spi_sck, mosi=spi_tx)
display = st7789.ST7789(spi0, disp_width, disp_width,
                          reset=machine.Pin(st7789_res, machine.Pin.OUT),
                          dc=machine.Pin(st7789_dc, machine.Pin.OUT),
                          xstart=0, ystart=0, rotation=0)
display.fill(st7789.BLUE)

# 定时器、姿态传感器、蜂鸣器初始化-------------------------------------------------------------------
mma = MMA7660()
mySong = music(song)
tim1 = machine.Timer() # 初始化定时器
tim1.init(freq=25, mode=machine.Timer.PERIODIC, callback=tim1callback)

# ----------------------------------------------------------------------------------------
display.fill(st7789.BLACK)
time_start=utime.time()
rf.ReadFile(display, 'pcb.bin', 0)
display.text(font1, str(round(thermometer(),1)), 208, 232, color=st7789.WHITE, background=st7789.BLACK)
time_end=utime.time()
print('totally cost: ',time_end-time_start)



while True:
    update_pose()
    utime.sleep(1)

每一个硬件驱动基本都封装成一个单独的文件,这样可以使顶层文件简洁明了,仅150余行。

总结

作品在功能上较好完成。3个功能都能够完全实现,程序能够正常运行,没有明显bug。程序结构简洁,代码清晰明了。

但性能上有所欠缺,突出体现在LCD屏的刷新速度上,尽管受限于硬件设计,但速度的上限没有完全逼近。

蜂鸣器的音乐没能自主设计,理论上可以将音乐文件保存在tf卡中进行读取,可以实现长音乐的播放。

已经将全部的代码文件及测试样例上传到附件当中。

参考资料也已全部上传,部分文件由于大小限制未能上传,请到树莓派官网下载。

致谢

首先感谢硬禾学堂提供这样一个机会,能够完成一个作品,在交流与思考中成长。感谢在我完成作品过程中提供帮助的人们,包括上文所提到的两位。还有其它在我困难时给予我帮助的人,由于他们的作品尚未审核,不能够罗列他们的链接。以及在树莓派PICO交流群中指导我的人们,恕我未能一一列举你们。

附件下载
参考资料.zip
参考的PICO资料,部分资料由于大小限制未能上传,请去树莓派官网下载
程序.zip
包括测试程序在内的所有代码,主程序为main.py文件和drive、fonts两个文件夹的内容
团队介绍
南京邮电大学是国家工业和信息化部与江苏省人民政府共建的高校。在长期的办学过程中,人才培养质量高,毕业生社会声誉好。
团队成员
大名
电子与光学工程学院、微电子学院
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号