基于Raspberry Pi Pico W与云端AI的嵌入式语音合成方案实践
1. 项目概述当微型单片机遇上AI语音合成最近在捣鼓一个智能家居的小项目需要一个能“说话”的节点但又不希望它体积庞大、功耗高或者成本离谱。于是我把目光投向了手边那枚小巧的Raspberry Pi Pico W。你可能知道它是个功能强大的微控制器但用它来跑AI驱动的文本转语音听起来是不是有点天方夜谭毕竟Pico W的核心是双核ARM Cortex-M0主频133MHz内存只有264KB的SRAM和2MB的板载闪存。这配置别说跑动辄数GB的大语言模型就连一个完整的现代TTS引擎都够呛。但这就是这个项目的魅力所在在资源极度受限的边缘设备上实现高质量的AI语音合成。我们并不是要在Pico W上本地训练或运行一个完整的神经网络模型——那确实不现实。核心思路是“云边协同”或“轻量化推理”。具体来说Pico W负责捕获文本信息、处理用户交互逻辑并通过其内置的Wi-Fi模块将文本发送到云端或局域网内一个更强大的AI服务进行处理接收回音频流或文件最后通过Pico的I/O口驱动一个简单的音频解码芯片或PWM模拟输出让设备“开口说话”。这解决了什么问题对于物联网设备、交互式玩具、低成本信息播报终端、无障碍辅助设备等场景它提供了一种极其灵活、低成本的语音反馈方案。你不再需要为每个设备集成昂贵的专用语音芯片或者依赖预录制的有限语音库。通过接入云端AI你可以获得近乎无限种声音、语调和语言的合成能力并且可以动态更新播报内容。适合谁呢适合所有对嵌入式开发、物联网和AI应用感兴趣的开发者、创客和学生。即使你AI背景不深也能通过这个项目亲手搭建一个连接物理世界与智能语音的桥梁。2. 核心方案选型与架构设计面对Pico W有限的资源直接本地运行如Tacotron、WaveNet这类现代神经TTS模型是天方夜谭。因此我们的方案设计必须围绕“卸载计算”和“高效通信”展开。主要有三种主流路径各有优劣。2.1 方案一云端API调用最灵活、效果最佳这是最直接、也是效果最好的方案。Pico W作为客户端通过HTTP/HTTPS请求将待合成的文本发送到云服务商的TTS API然后接收返回的音频文件通常是MP3或WAV格式再进行本地播放。为什么首选这个方案质量与多样性像Azure Cognitive Services、Google Cloud Text-to-Speech、Amazon Polly等提供的语音其自然度和丰富度是当前边缘侧模型难以比拟的。它们支持多种语言、方言、情感音色甚至定制声音。零模型部署负担我们完全不用关心模型的大小、优化和部署只需关注如何调用API和解析返回数据。开发速度快各大云平台都提供了完善的SDK和文档集成相对简单。Pico W端的核心任务网络连接使用network和socket库连接Wi-Fi。HTTP客户端构造一个携带文本、语言、音色等参数的POST请求。这里要注意Pico的MicroPython环境内存有限处理大块的音频数据时需要流式接收或分块处理避免内存溢出。音频解码与播放云端返回的往往是压缩音频如MP3。Pico W没有硬件音频解码器所以我们需要方案A软件解码在Pico上运行一个轻量级的MP3解码库如helix-mp3的MicroPython移植版。这会消耗可观的CPU资源和内存可能影响其他任务且解码质量一般。方案B外接解码芯片更推荐。使用一个如VS1053B或MAX98357A这类I2S音频解码/放大器模块。Pico W通过I2S总线发送原始的PCM数据流给芯片由芯片完成高质量的数模转换和放大。这样Pico只需负责存储和传输原始的PCM数据压力小很多。2.2 方案二局域网内服务器中转兼顾隐私与延迟如果你对数据隐私有要求或者希望在没有互联网的环境下使用可以在本地局域网部署一个TTS服务Pico W与之通信。如何实现服务器选择在一台树莓派、旧电脑甚至NAS上部署开源的TTS引擎。例如Coqui TTS一个强大的开源TTS工具包支持许多预训练模型可以在x86或ARM设备上运行。Piper一个速度极快、质量不错的本地TTS系统用C编写在树莓派4上就能流畅运行合成速度远超实时。通信协议Pico W通过HTTP向本地服务器的特定端口如http://192.168.1.100:5500/synthesize发送请求。服务器合成后将音频文件返回。优势数据不出局域网延迟通常比云端更低且可自定义模型。Pico W端的任务与方案一类似只是请求的地址变成了内网IP。2.3 方案三极简本地合成资源消耗的极限挑战这是最硬核、最受限制的方案旨在完全离线运行。我们必须在Pico W上运行一个极度轻量化的TTS算法。可能的技术路径拼接合成预录制所有音素或音节根据文本动态拼接播放。效果生硬需要大量存储空间存放音频片段不灵活。参数合成使用如MBROLA或eSpeak NG这类老式的、基于共振峰合成的引擎。它们有C语言实现可以被移植到MicroPython或直接用C SDK编写。eSpeak NG本身非常小巧合成的是“机器人”声音清晰可懂但自然度差。有人成功将其移植到了ESP32上Pico W在性能上与之接近理论上可行。微型神经网络寻找或训练一个参数量在几十万到一百万级别的超轻量级TTS模型例如基于LPCNet或类似结构然后使用TensorFlow Lite for Microcontrollers或MicroTVM进行部署。这属于前沿探索难度极高需要对模型压缩、量化有深入理解且合成质量是重大挑战。注意对于大多数应用方案一云端API是平衡效果、开发难度和成本的最佳选择。方案二适合进阶玩家和对隐私有要求的场景。方案三则更像是一个学术或极限优化的挑战项目。本博文后续的实操部分将基于方案一Azure TTS API结合方案B外接I2S音频芯片来展开因为这是最能体现“AI能力”与“嵌入式落地”结合且复现成功率最高的路径。3. 硬件准备与电路连接工欲善其事必先利其器。除了Pico W本体我们还需要一些外围硬件来让它“发声”。3.1 硬件清单Raspberry Pi Pico W主角负责逻辑控制和网络通信。I2S音频解码放大模块这是关键。推荐使用MAX98357A模块。它集成了I2S接口、DAC和3W D类放大器价格低廉接线简单且与Pico W兼容性极好。VS1053B功能更强可解码多种格式但接线和驱动稍复杂。扬声器一个4Ω或8Ω功率在3W左右的小喇叭。直接连接到MAX98357A的扬声器输出端。电源确保你的电源如USB线或电池能提供足够的电流。Pico W峰值电流约100mAMAX98357A驱动喇叭时可能达到数百mA所以建议总供电能力不低于500mA。面包板与杜邦线用于连接。Micro-USB数据线用于给Pico W供电和编程。3.2 电路连接详解以MAX98357A为例MAX98357A模块通常有以下几个引脚VIN电源输入3.3V-5V。重要必须与Pico W共地。GND地线。BCLK(Bit Clock)I2S位时钟。LRC(Left/Right Clock)I2S左右声道时钟字选信号。DIN(Data In)I2S数据输入。GAIN增益设置引脚悬空、接高或接低以设置增益倍数。SD(Shutdown)关机引脚高电平有效我们通常接低电平(GND)使其一直工作。与Pico W的连接对应关系MAX98357A 引脚Pico W GPIO 引脚功能说明VINVBUS(Pin 40) 或外部5V供电。接VBUS是从USB取电。GND任意GND(如 Pin 38)共地。必须连接BCLKGPIO 18(Pin 24)I2S位时钟。LRCGPIO 19(Pin 25)I2S左右声道时钟。DINGPIO 20(Pin 26)I2S数据输入。GAIN悬空或通过电阻配置悬空时增益约为9dB。SD接GND使能芯片一直工作。为什么选择这些GPIO在MicroPython中Pico W的I2S功能是绑定在特定引脚上的。上述GPIO 18, 19, 20是硬件I2S0的默认数据引脚使用它们能获得最佳性能和兼容性。你也可以使用其他支持I2S的引脚但需要在代码中明确指定。实操接线心得在接电前务必再三检查VIN和GND没有接反否则会瞬间损坏模块。如果播放时噪音很大很可能是地线干扰。确保Pico W和MAX98357A的GND用短而粗的线可靠连接。如果使用面包板电源路径上的接触电阻也可能引入噪音。如果声音小或失真检查GAIN引脚配置。根据数据手册GAIN悬空默认增益约为9dB如果接VIN是15dB接GND是6dB。对于小喇叭9dB通常足够。4. 软件环境搭建与基础代码我们将使用MicroPython进行开发因为它对网络和硬件外设的操作比C/C更友好快捷。4.1 刷写MicroPython固件按住Pico W上的BOOTSEL按钮同时将其通过USB连接到电脑。松开按钮电脑会识别出一个名为RPI-RP2的U盘。从MicroPython官网下载最新的Raspberry Pi Pico W固件文件.uf2格式。将下载的.uf2文件拖入RPI-RP2U盘。Pico W会自动重启并运行MicroPython。4.2 开发工具与基础库推荐使用Thonny这款IDE。它内置了MicroPython支持可以方便地连接Pico W、上传文件和实时交互调试。连接Thonny后我们需要准备几个关键的软件库urequests(或requests)MicroPython中用于HTTP请求的库。通常固件已内置urequests它是一个简化版的requests。I2S驱动MicroPython的machine模块内置了I2S类我们直接使用即可。Wi-Fi连接使用network模块。首先我们来编写一个连接Wi-Fi的脚本boot.py或main.py的开头部分import network import time # 你的Wi-Fi凭证 WIFI_SSID 你的Wi-Fi名称 WIFI_PASSWORD 你的Wi-Fi密码 def connect_wifi(): wlan network.WLAN(network.STA_IF) wlan.active(True) if not wlan.isconnected(): print(正在连接到网络...) wlan.connect(WIFI_SSID, WIFI_PASSWORD) # 等待连接最多10秒 max_wait 10 while max_wait 0: if wlan.isconnected(): break max_wait - 1 print(等待连接..., max_wait) time.sleep(1) if wlan.isconnected(): print(网络连接成功) print(IP地址:, wlan.ifconfig()[0]) return True else: print(网络连接失败) return False # 开机尝试连接 if connect_wifi(): # 连接成功继续执行其他任务 pass else: # 连接失败进入深度睡眠或等待重启 machine.deepsleep(10000) # 示例深度睡眠10秒4.3 I2S音频播放基础框架在连接Wi-Fi之后我们需要初始化I2S接口为播放音频做好准备。from machine import I2S, Pin # 初始化I2S # 参数说明 # sckPin(18) # 位时钟引脚 # wsPin(19) # 左右声道时钟引脚 # sdPin(20) # 数据引脚 # modeI2S.TX # 发送模式 # bits16 # 采样位深 # formatI2S.MONO # 单声道MAX98357A支持 # rate22050 # 采样率需与音频数据匹配 # ibuf20000 # 内部缓冲区大小字节 i2s I2S(0, sckPin(18), wsPin(19), sdPin(20), modeI2S.TX, bits16, formatI2S.MONO, rate22050, ibuf20000) def play_audio_from_buffer(audio_buffer): 将音频数据缓冲区写入I2S i2s.write(audio_buffer) # audio_buffer应为bytes类型 def stop_and_deinit(): 停止播放并释放I2S资源 i2s.deinit()关键参数解析bits和format必须与你的音频数据格式一致。我们从云端API获取的通常是16位、单声道PCM数据。rate采样率。这是最容易出错的地方Azure TTS等服务的默认输出可能是24kHz或22.05kHz。你必须在代码中设置与音频数据完全相同的采样率否则播放速度会不对声音会像卡通片或慢放。通常需要在请求API时指定输出格式并在代码中写死对应的采样率。ibuf内部缓冲区大小。如果播放不流畅卡顿可以适当增大这个值。但Pico W内存有限不宜过大通常20000-50000字节是个安全范围。5. 集成云端TTS API以Azure为例现在进入核心环节让Pico W从云端获取语音。我们以微软Azure Cognitive Services的文本转语音API为例因为它提供了每月一定量的免费额度非常适合学习和原型开发。5.1 创建Azure TTS资源并获取密钥访问Azure门户创建一个“语音服务”资源。创建成功后在资源的“密钥和终结点”页面找到你的区域如eastus和两个密钥之一。我们后续需要用到。5.2 构造HTTP请求Azure TTS API支持SSML功能强大。一个最简单的请求是获取指定文本的音频流。import urequests import ujson # Azure TTS 配置 AZURE_REGION eastus # 你的资源区域 AZURE_KEY 你的Azure语音服务密钥 TTS_ENDPOINT fhttps://{AZURE_REGION}.tts.speech.microsoft.com/cognitiveservices/v1 # 请求头 headers { Ocp-Apim-Subscription-Key: AZURE_KEY, Content-Type: application/ssmlxml, X-Microsoft-OutputFormat: riff-16khz-16bit-mono-pcm, # 指定输出为16kHz, 16bit, 单声道PCM WAV User-Agent: RaspberryPiPicoW-TTS } # 请求体 (SSML) def build_ssml(text, voice_nameen-US-JennyNeural): 构建SSML XML字符串 # 注意XML中的双引号需要转义或者外层用单引号 return f speak version1.0 xml:langen-US voice name{voice_name} {text} /voice /speak def synthesize_and_play(text): 合成语音并播放 # 1. 构建SSML ssml_body build_ssml(text) # 2. 发送POST请求 print(正在向Azure TTS发送请求...) try: response urequests.post(TTS_ENDPOINT, headersheaders, datassml_body) except Exception as e: print(网络请求失败:, e) return False # 3. 检查响应 if response.status_code 200: print(音频获取成功开始播放...) # 4. 获取音频数据 (WAV格式包含44字节的头部) audio_data response.content # 5. 剥离WAV文件头获取纯PCM数据 # 一个标准的44字节WAV头PCM数据从第44字节开始 pcm_data audio_data[44:] if len(audio_data) 44 else audio_data # 6. 通过I2S播放 play_audio_from_buffer(pcm_data) # 调用之前定义的播放函数 print(播放完成。) response.close() return True else: print(TTS请求失败状态码:, response.status_code) print(错误信息:, response.text) response.close() return False # 示例合成并播放一句话 if __name__ __main__: connect_wifi() # 确保已连接Wi-Fi synthesize_and_play(Hello from Raspberry Pi Pico W and Azure Cognitive Services!)代码关键点解析X-Microsoft-OutputFormat这个头至关重要。我们选择了riff-16khz-16bit-mono-pcm。它告诉Azure返回一个16kHz采样率、16位深、单声道的未压缩PCM WAV流。这个格式与我们的I2S配置rate16000完全匹配无需软件解码可以直接播放。剥离WAV头API返回的是完整的WAV文件包含一个44字节的文件头。I2S驱动只需要纯粹的PCM音频数据。所以我们需要手动跳过前44个字节。这是一个常见的“坑”。错误处理网络请求可能失败API可能返回错误。用try...except包裹请求并检查status_code是生产级代码的基本素养。内存管理response.content会将整个音频文件读入内存。对于很长的文本音频文件可能很大几秒的语音就几十KB。Pico W的264KB内存可能吃紧。对于长文本应考虑流式接收response.raw.read(chunk_size)并分块播放但这会显著增加代码复杂度。初期建议先合成短句。5.3 适配I2S采样率注意上面的代码中Azure输出的是16kHz的PCM。因此你必须回头修改I2S初始化的rate参数将其从22050改为16000否则播放的声音会又尖又快。i2s I2S(0, sckPin(18), wsPin(19), sdPin(20), modeI2S.TX, bits16, formatI2S.MONO, rate16000, # 修改为16000以匹配Azure输出 ibuf20000)6. 系统集成与优化实践将各个模块组合起来形成一个稳定、可用的系统需要考虑更多细节。6.1 完整的工作流脚本创建一个main.py整合所有功能import network import time import urequests from machine import I2S, Pin import ujson # 配置区 WIFI_SSID Your_WiFi WIFI_PASS Your_Password AZURE_KEY Your_Azure_Key AZURE_REGION eastus # 硬件初始化 def init_hardware(): global i2s # 初始化I2S (16kHz匹配Azure PCM输出) i2s I2S(0, sckPin(18), wsPin(19), sdPin(20), modeI2S.TX, bits16, formatI2S.MONO, rate16000, ibuf30000) # 缓冲区稍大一些 print(I2S音频初始化完成。) # Wi-Fi连接 (同上略) def connect_wifi(): # ... (复用之前的代码) pass # TTS合成与播放 (同上略) def synthesize_and_play(text, voiceen-US-JennyNeural): # ... (复用之前的代码注意要使用全局变量i2s) pass # 主循环 def main(): print(系统启动...) if not connect_wifi(): print(无法连接Wi-Fi系统停止。) return init_hardware() # 示例播放欢迎语 synthesize_and_play(System is ready.) time.sleep(1) # 这里可以加入你的主业务逻辑 # 例如读取传感器根据条件播放不同语音 # while True: # if some_condition: # synthesize_and_play(Temperature is high.) # time.sleep(5) # 或者作为一个简单的网络TTS终端从UART接收文本 # import sys # while True: # text sys.stdin.readline().strip() # if text: # synthesize_and_play(text) if __name__ __main__: main()6.2 功耗与性能优化技巧Pico W本身功耗很低但Wi-Fi和I2S播放时功耗会上升。对于电池供电设备优化至关重要。Wi-Fi连接管理仅在需要时连接不要一直保持Wi-Fi连接。在需要合成语音前连接Wi-Fi完成后调用wlan.disconnect()和wlan.active(False)来关闭Wi-Fi射频可以大幅省电。使用深度睡眠如果设备长时间待机可以考虑使用machine.deepsleep()并定时唤醒。但注意深度睡眠会丢失RAM状态程序会从头开始运行。音频播放优化缓冲区策略ibuf大小需要权衡。太小会导致播放卡顿缓冲区欠载太大会占用过多内存并增加初始延迟。通过实验找到一个稳定播放的最小值。预加载与流式播放对于固定提示音如“欢迎”、“错误”可以预先将合成好的PCM数据以字节数组的形式存储在代码中或Pico的闪存里需要时直接播放省去网络请求时间和流量。这需要使用open(sound.bin, rb)读取二进制文件到内存。内存管理Pico W的264KB RAM是最大限制。使用import gc; gc.collect()可以手动触发垃圾回收在长时间运行或处理大块数据后调用有助于避免内存碎片化导致分配失败。避免在函数内创建大的临时对象如巨大的字符串或字节数组尽量复用全局缓冲区。6.3 扩展功能支持更多语音与本地触发多语言/多音色支持只需修改build_ssml函数中的voice_name参数。Azure支持上百种声音如zh-CN-XiaoxiaoNeural中文女声、ja-JP-NanamiNeural日文女声等。你可以根据需求动态切换。本地触发播放结合一个按钮GPIO输入或传感器如PIR运动传感器实现物理触发。from machine import Pin button Pin(15, Pin.IN, Pin.PULL_UP) # 按钮接GPIO15上拉 while True: if button.value() 0: # 按钮被按下 synthesize_and_play(Button pressed!) time.sleep(0.5) # 简单防抖从网络获取文本让Pico W作为一个网络客户端从指定的服务器URL获取需要播报的文本实现远程信息推送。def get_text_from_server(url): resp urequests.get(url) if resp.status_code 200: return resp.text.strip() return None text_to_speak get_text_from_server(http://your-server.com/get-announcement) if text_to_speak: synthesize_and_play(text_to_speak)7. 常见问题与深度排查指南在实际操作中你几乎一定会遇到下面这些问题。这里是我踩过坑后的经验总结。7.1 问题一无声或全是噪音这是最常见的问题排查链如下电源与地线这是硬件问题之首。确保扬声器、MAX98357A、Pico W三者共地良好。用万用表蜂鸣档检查地线连通性。尝试用手机充电器或电脑USB口等不同电源供电排除电源功率不足或噪声大的问题。音量与增益检查MAX98357A的GAIN引脚配置。尝试用跳线帽将其接VIN高增益或GND低增益测试。确保代码中I2S输出的音量数据不是全零静音。I2S配置错误引脚接错再三核对BCLK,LRC,DIN是否与代码中定义的GPIO号对应。采样率不匹配这是软件层面最高频的错误源你必须百分之百确认Azure API输出的音频格式如riff-16khz-16bit-mono-pcm与I2S初始化时的rate必须是16000一致。不一致会导致播放速度错误可能表现为尖锐噪音或低沉杂音。数据格式不匹配确认bits16和formatI2S.MONO与音频数据一致。Azure的PCM输出是16位有符号整数小端序。WAV头未剥离如果你播放的是从API获取的完整WAV响应必须跳过前44字节标准PCM WAV头。播放包含头部的数据会产生“砰”的一声巨响然后是奇怪的噪音。代码逻辑在i2s.write()调用后是否有time.sleep()确保播放完成I2S.write是非阻塞的它启动DMA传输后就返回了。如果紧接着关闭I2S或重置设备音频会被截断。可以用while i2s.write()检查是否写完或者简单time.sleep(音频时长)。7.2 问题二播放卡顿、断断续续网络延迟或抖动Wi-Fi信号不稳定会导致下载音频数据变慢。可以在代码中打印网络请求耗时。考虑在播放前预加载完整音频数据到内存而不是边下边播。I2S缓冲区ibuf太小这是主因。I2S以恒定速率消耗数据如果网络或代码供给数据的速度跟不上缓冲区变空就会卡顿。逐步增加ibuf值如从20000到50000观察改善情况。但注意不要超过可用内存。内存不足导致GC垃圾回收MicroPython在内存紧张时会自动进行垃圾回收这是一个“停止世界”的操作会短暂阻塞所有代码执行导致音频流中断。如果你在播放音频的循环中频繁创建新的对象如字符串、列表就可能触发GC。解决方案使用micropython.mem_info()查看内存使用情况。将固定的字符串、配置等定义为全局常量。在播放开始前手动调用gc.collect()清空一次内存。优化代码减少循环内临时对象的创建。CPU过载如果你的主循环中除了播放音频还有非常密集的计算任务如复杂的传感器数据处理可能会抢占CPU导致I2S的DMA中断服务不及时。考虑将非实时任务放到播放间隙执行。7.3 问题三声音失真、音调不对采样率问题再次强调音调不对变高像老鼠叫变低像巨人几乎可以断定是采样率设置错误。仔细检查API请求头中的OutputFormat和I2S的rate。电源电压不足当MAX98357A驱动喇叭发出较大声音时瞬时电流需求增大。如果电源线太长太细或USB口供电能力弱会导致电压被拉低引起声音破音失真。尝试靠近电源端并接一个100-470uF的电解电容到VIN和GND之间进行电源退耦。音频数据本身问题极少数情况下从网络接收的音频数据可能因传输错误而损坏。可以尝试将接收到的音频数据先保存到Pico的文件系统然后在电脑上播放检查以隔离问题。7.4 问题四Wi-Fi连接不稳定信号强度Pico W的Wi-Fi天线是PCB上的走线信号强度一般。确保设备离路由器不要太远中间障碍物不要太多。重连机制在生产代码中绝不能假设一次连接就永远成功。必须实现重连逻辑。def ensure_wifi_connected(): wlan network.WLAN(network.STA_IF) if not wlan.isconnected(): print(Wi-Fi断开尝试重连...) connect_wifi() # 复用之前的连接函数 return wlan.isconnected() # 在每次需要网络操作前调用 if ensure_wifi_connected(): synthesize_and_play(Hello) else: print(网络不可用播放本地提示音) play_local_sound(error.wav)DHCP问题有时DHCP获取IP会失败。可以尝试设置静态IP或者增加连接等待时间。7.5 进阶调试技巧逻辑分析仪是神器如果你有逻辑分析仪甚至一些示波器带逻辑分析功能连接到BCLK,LRC,DIN引脚可以直观地看到I2S信号波形确认数据是否正确发送。这是排查硬件通信问题的终极手段。分步测试将问题分解。先写一个简单的测试程序用I2S播放一段你预先知道正确的、存储在Pico里的PCM数据比如一个简短的“嘀”声。如果这个能响证明硬件和I2S驱动没问题。再测试网络部分只请求API并把返回的音频数据长度打印出来或者保存到文件在电脑上验证。最后将两者结合。利用Thonny的REPLThonny的交互式命令行REPL非常好用。你可以在运行过程中随时中断程序检查变量状态、手动执行函数对于调试网络请求结果、查看内存非常方便。这个项目就像在针尖上跳舞在Pico W有限的资源内协调网络、音频和逻辑控制。每一个环节的疏忽都可能导致失败。但一旦调试成功听到清晰、自然的AI语音从这个小板子里发出来时那种成就感是无与伦比的。它不仅仅是一个功能实现更是一次对嵌入式系统资源管理、协议理解和问题排查能力的全面锻炼。你可以在此基础上把它集成到你的机器人、天气站、智能门铃中让冷冰冰的设备拥有温暖的人声交互能力。