STM32F103C8T6语音万年历实战:RTC+OLED+SU-03T从接线到调试 1. 为什么这个语音万年历项目值得动手做一遍如果你手头正好有一块STM32F103C8T6最小系统板又刚学完GPIO和定时器想找一个能把多个外设串起来练手的项目语音万年历是个很合适的选择。它不像单纯的流水灯那样学不到东西也不像复杂的物联网网关那样一上来就劝退。这个项目把RTC实时时钟、OLED显示、语音识别模块、按键交互这几块内容揉在一起做完之后你对STM32的I2C通信、串口通信、中断处理、状态机设计都会有一个完整的认识。我最早接触这类项目是在帮学弟调毕业设计的时候他用的就是STM32加OLED加语音模块的方案。当时他卡在OLED不亮这个问题上整整两天后来发现是I2C地址搞错了。这件事让我意识到这类项目看起来简单但细节坑特别多网上很多教程只给代码不讲原理出了问题根本不知道怎么排查。所以这篇文章我打算把每个环节的“为什么”都讲清楚让你不光能跑通还能在出问题的时候自己定位。这个项目最终实现的效果是这样的OLED屏幕上显示年月日、星期、时分秒温度信息也可以加上你可以通过按键调整时间还可以对着语音模块说“现在几点”“今天几号”模块识别后通过串口把指令发给STM32单片机解析后控制OLED显示对应信息或者用语音播报出来。整套东西成本不高一块STM32核心板十几块0.96寸OLED屏不到十块语音模块用SU-03T的话也就二十出头加起来五十块以内能搞定。适合谁来参考这篇内容如果你已经会用Keil或者VSCode加PlatformIO写STM32的基础程序知道怎么点灯、怎么用定时器那这篇内容你跟着做下来会很顺。如果你连开发环境都还没搭好建议先把环境配置和GPIO操作搞定再回来。另外如果你正在做毕业设计或者课程设计这个项目作为一个基础框架往上加DHT11温湿度、加BH1750光照传感器、加MQ-2烟雾检测都很方便扩展性很好。2. 硬件选型与接线方案每个模块为什么这么选2.1 主控芯片STM32F103C8T6还是别的STM32F103C8T6这颗芯片可以说是入门32位单片机的“国民型号”48个引脚72MHz主频64KB Flash20KB RAM对于万年历这个项目来说资源绰绰有余。它自带RTC外设虽然精度一般但配合外部32.768kHz晶振做时钟源走时精度完全够日常使用。更重要的是它的I2C、USART、TIM资源丰富同时驱动OLED和语音模块不会出现外设不够用的情况。有人可能会问能不能用STM32F401或者G030当然可以但F103的资料最多社区里遇到问题最容易搜到答案。而且F103C8T6的最小系统板便宜且到处都有BluePill板型引脚布局也规范插面包板很方便。如果你手头只有别的型号代码移植的主要工作量在HAL库的初始化配置上逻辑层几乎不用改。注意市面上有些F103C8T6的板子用的是CH340串口芯片有些用的是STM32自带的USB转串口下载程序时驱动不一样买之前确认一下。2.2 显示方案0.96寸OLED还是LCD1602选OLED而不是LCD1602理由很直接。第一OLED是自发光不需要背光电路对比度高在暗光环境下看时间很清晰。第二0.96寸OLED分辨率128x64能显示4行16x16的汉字或者8行ASCII字符信息密度比LCD1602的2行16字符高得多。第三OLED用I2C接口只需要两根线接线简单而LCD1602并行接口至少要6根线占引脚不说面包板上走线也乱。不过OLED有个通病就是I2C地址问题。市面上常见的0.96寸OLED模块用的驱动芯片有SSD1306和SH1106两种SSD1306的I2C地址通常是0x788位地址或者0x3C7位地址SH1106的地址可能是0x78或者0x7A。如果你买到的模块背面没有标注可以用I2C扫描程序先扫一下地址。我遇到过一块模块标注的是SSD1306但实际是SH1106初始化后屏幕偏移了两列后来换了SH1106的初始化序列才正常。2.3 语音模块SU-03T和LD3320怎么选语音识别模块的选择直接决定了项目的交互体验。目前常见的有两种方案一种是SU-03T这类离线语音识别模块另一种是LD3320。SU-03T的优势在于它内置了语音识别引擎你只需要在它的配置工具里设置好词条比如“现在几点”“今天几号”“设置时间”模块识别到之后通过串口输出对应的指令码STM32收到指令码执行相应操作就行。整个过程不需要联网响应速度快延迟基本在几百毫秒以内。LD3320是另一款经典的语音识别芯片但它需要STM32通过SPI或者并口去读写寄存器开发难度比SU-03T大不少而且识别率受环境影响比较明显。如果你只是想快速实现功能SU-03T是更省心的选择。它的配置工具是图形化的添加词条、设置串口波特率、生成固件、烧录整个流程半小时能搞定。提示SU-03T的串口默认波特率通常是9600但可以在配置工具里改。STM32这边的串口初始化要和它保持一致否则收到的全是乱码。2.4 接线方案与引脚分配我把整个系统的接线整理成了一张表你可以直接照着接。这里假设用的是STM32F103C8T6最小系统板OLED用I2C1语音模块用USART2按键用GPIO输入。模块引脚STM32引脚说明OLEDSCLPB6I2C1时钟OLEDSDAPB7I2C1数据OLEDVCC3.3V供电OLEDGNDGND共地SU-03TTXPA3USART2_RXSU-03TRXPA2USART2_TXSU-03TVCC5V模块供电SU-03TGNDGND共地按键1一端PA0模式切换按键2一端PA1加按键3一端PA4减按键另一端GND按下为低电平这里解释一下为什么语音模块用USART2而不是USART1。USART1的PA9和PA10通常用来接串口下载或者调试打印如果被语音模块占了调试信息就没地方输出了。用USART2可以把调试串口和语音串口分开调试的时候方便看日志。另外PA0到PA4这几个引脚在F103上都是普通GPIO配置成上拉输入配合按键到地按下时读低电平不需要外部上拉电阻。3. RTC时钟配置让时间走得准的底层逻辑3.1 RTC时钟源的选择与精度分析STM32F103的RTC有三种时钟源可选HSE分频、LSE外部32.768kHz晶振、LSI内部40kHz RC振荡器。HSE分频虽然精度高但一旦主电源断了RTC就不走了因为HSE是主时钟的一部分。LSI是内部RC振荡不需要外部元件但精度很差一天可能差几十秒甚至几分钟而且随温度漂移严重。LSE外接32.768kHz晶振是最常用的方案精度取决于晶振本身的品质普通晶振一个月误差在几分钟以内好一点的晶振可以做到一个月误差几十秒。我建议用LSE加外部晶振的方案。32.768kHz这个频率不是随便选的因为32768是2的15次方经过15次二分频正好得到1Hz的秒信号RTC计数器每秒加一硬件设计上很自然。晶振选那种圆柱形的32.768kHz晶振负载电容一般选6pF或者12.5pF具体看晶振规格书。PCB布局的时候晶振要尽量靠近芯片的PC14和PC15引脚走线短且远离干扰源。3.2 备份域与掉电保持RTC属于备份域由VBAT引脚供电。如果你希望断电之后时间不丢失需要在VBAT引脚上接一颗纽扣电池比如CR1220或者CR2032。没有电池的话主电源一断RTC寄存器的值就清零了下次上电时间就回到初始值。接电池的时候注意正负极不要接反VBAT引脚接电池正极电池负极接GND。备份域还有一个坑第一次上电或者电池没电之后备份域寄存器是未定义的RTC可能不工作。所以初始化的时候要先使能PWR和BKP时钟然后调用HAL_PWR_EnableBkUpAccess()解除备份域写保护再检查RTC是否已经配置过。如果没配置过才执行RTC初始化。这个判断逻辑很重要否则每次上电都重新配置RTC时间就被重置了。3.3 HAL库RTC初始化代码拆解下面这段代码是RTC初始化的核心部分我加了详细注释说明每一步在做什么。/* 使能PWR和BKP时钟 */ __HAL_RCC_PWR_CLK_ENABLE(); __HAL_RCC_BKP_CLK_ENABLE(); /* 解除备份域写保护 */ HAL_PWR_EnableBkUpAccess(); /* 检查RTC是否已经配置过 */ if (HAL_RTC_Init(hrtc) ! HAL_OK) { /* 初始化失败可能是备份域复位了 */ Error_Handler(); } /* 如果RTC没有配置过设置初始时间 */ if (__HAL_RTC_IS_RTC_RESET() SET) { /* 设置日期2025年1月1日星期三 */ sDate.Year 25; sDate.Month RTC_MONTH_JANUARY; sDate.Date 1; sDate.WeekDay RTC_WEEKDAY_WEDNESDAY; HAL_RTC_SetDate(hrtc, sDate, RTC_FORMAT_BIN); /* 设置时间12时0分0秒 */ sTime.Hours 12; sTime.Minutes 0; sTime.Seconds 0; HAL_RTC_SetTime(hrtc, sTime, RTC_FORMAT_BIN); }这里有个细节要注意__HAL_RTC_IS_RTC_RESET()这个宏在HAL库的不同版本里名字可能不一样有的版本是__HAL_RTC_IS_RTC_RESET()有的是通过读RTC_ISR寄存器的INITS标志来判断。如果你编译报错说找不到这个宏可以直接读寄存器判断。3.4 时间读取与格式转换RTC读出来的时间是BCD格式还是二进制格式取决于初始化时传的参数。上面代码里用的是RTC_FORMAT_BIN读出来就是普通的十进制数。如果你用的是RTC_FORMAT_BCD读出来的值需要转换。我建议统一用BIN格式省得每次都要转换。读取时间的函数调用很简单HAL_RTC_GetTime(hrtc, sTime, RTC_FORMAT_BIN); HAL_RTC_GetDate(hrtc, sDate, RTC_FORMAT_BIN);但要注意一个坑HAL_RTC_GetTime和HAL_RTC_GetDate必须成对调用而且要先调GetTime再调GetDate。因为读时间的时候硬件会把日期锁存到影子寄存器里如果只读时间不读日期下次读时间可能读到旧值。这个细节在ST的参考手册里有说明但很多人不看手册直接抄代码结果时间偶尔跳变或者不更新排查半天找不到原因。4. OLED显示驱动从点亮到显示汉字的完整路径4.1 I2C通信基础与OLED初始化序列OLED模块本质上是一块SSD1306驱动的128x64点阵屏STM32通过I2C往SSD1306的寄存器写命令和数据。I2C的时序HAL库已经封装好了你只需要调用HAL_I2C_Mem_Write就行。但SSD1306的初始化序列比较长大概二十多条命令每条命令的作用不一样。初始化序列里几个关键命令我解释一下。0xAE是关闭显示0xD5设置时钟分频0xA8设置多路复用率对于128x64的屏值是0x3F0x20设置内存寻址模式通常用0x00水平寻址或者0x02页寻址0x8D是电荷泵设置必须发0x14开启电荷泵否则屏幕不亮0xAF是开启显示。很多人OLED不亮就是因为漏了电荷泵那条命令或者电荷泵参数发错了。/* OLED初始化命令序列部分 */ static const uint8_t oled_init_cmds[] { 0xAE, /* 关闭显示 */ 0xD5, 0x80, /* 设置时钟分频 */ 0xA8, 0x3F, /* 设置多路复用率 */ 0xD3, 0x00, /* 设置显示偏移 */ 0x40, /* 设置起始行 */ 0x8D, 0x14, /* 电荷泵使能 */ 0x20, 0x00, /* 水平寻址模式 */ 0xA1, /* 段重映射 */ 0xC8, /* 行重映射 */ 0xDA, 0x12, /* 设置COM引脚配置 */ 0x81, 0xCF, /* 设置对比度 */ 0xD9, 0xF1, /* 设置预充电周期 */ 0xDB, 0x40, /* 设置VCOMH */ 0xA4, /* 全局显示开启 */ 0xA6, /* 正常显示非反色 */ 0xAF /* 开启显示 */ };4.2 显存管理与页地址模式SSD1306内部有一块128x64位的显存GDDRAM总共1024字节分成8页每页128字节每字节对应8个像素点。往显存写数据的时候可以按页写也可以按水平模式写。页地址模式下你指定页号和列号然后连续写该页的数据写完一页再换下一页。水平模式下你指定起始页和结束页、起始列和结束列然后连续写地址会自动递增跨页也会自动换行。我一般用水平寻址模式因为刷整屏的时候方便设置好窗口之后一次性把1024字节发过去就行。但如果你只更新局部区域比如只更新时间那一行用页地址模式更灵活不需要刷新整个屏幕。实际项目中我建议维护一个显存缓冲区所有绘制操作先写到缓冲区里最后统一刷到OLED这样避免频繁的I2C通信拖慢主循环。4.3 汉字取模与显示原理OLED显示汉字需要先取模。取模的意思是把一个16x16点阵的汉字转换成32字节的二进制数据每个字节代表一行中的8个像素。取模软件推荐用PCtoLCD2002设置的时候选“阴码”“逐列式”“顺向”输出格式选C51格式。取模之后你会得到一组十六进制数组比如“年”字的取模数据是32个字节。显示的时候把取模数据按页写入显存。一个16x16的汉字占两页每页16字节。第一页是上半部分第二页是下半部分。写入的时候先设置页地址和列地址然后连续写16字节再换到下一页写另外16字节。/* 在指定位置显示一个16x16汉字 */ void OLED_ShowChinese(uint8_t page, uint8_t col, const uint8_t *hz) { uint8_t i; /* 上半部分 */ OLED_SetCursor(page, col); for (i 0; i 16; i) { OLED_WriteData(hz[i]); } /* 下半部分 */ OLED_SetCursor(page 1, col); for (i 0; i 16; i) { OLED_WriteData(hz[i 16]); } }4.4 显示布局设计与刷新策略万年历的显示布局我建议这样安排第一行显示日期和星期比如“2025-01-01 周三”第二行显示时间比如“12:00:00”第三行显示温度或者农历信息第四行留给语音交互的提示信息比如“请说指令”。这样四行刚好把128x64的屏幕填满信息层次也清晰。刷新策略上不要每次主循环都刷整屏那样I2C通信量太大屏幕看起来会有闪烁感。我的做法是只刷新变化的部分。比如秒数变了只更新秒数那两个字符的位置日期变了只更新日期那一行。用一个标志位记录哪些区域需要刷新主循环里检查标志位有变化才刷。这样CPU占用低显示也稳定。提示OLED长时间显示固定内容会有烧屏风险虽然0.96寸小屏不太明显但如果你打算7x24小时运行建议每隔一段时间微调一下显示位置或者降低对比度。5. 语音识别模块的串口对接与指令解析5.1 SU-03T配置流程与词条设计SU-03T的配置工具叫“智能语音配置工具”打开之后新建项目选择芯片型号SU-03T然后进入词条配置界面。词条就是你希望模块能识别的语音指令比如“现在几点”“今天几号”“设置时间”“增加”“减少”“确认”。每个词条有一个ID识别到之后模块会通过串口输出对应的ID码。词条设计有几个经验。第一词条之间发音差异要大避免“几点”和“几号”这种听起来接近的词条容易误识别。第二可以给同一个词条添加多个说法比如“现在几点”和“几点了”都映射到同一个ID提高识别率。第三串口输出格式可以选“ID数据”或者“纯ID”我建议用纯ID模式STM32这边解析简单收到一个字节就知道是什么指令。配置完成之后点击“生成固件”然后用烧录工具把固件写到SU-03T模块里。烧录的时候模块要进入下载模式通常是按住某个按键再上电具体看模块说明书。5.2 STM32串口接收中断配置STM32这边用USART2接收语音模块的数据配置波特率9600、8位数据位、1位停止位、无校验。接收方式我推荐用中断接收每收到一个字节进一次中断把数据存到缓冲区里。如果用轮询方式主循环会被阻塞影响OLED刷新和按键响应。/* USART2中断接收回调 */ void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART2) { /* 把收到的字节存入环形缓冲区 */ ring_buf_put(voice_rx_buf, voice_rx_byte); /* 重新开启接收中断 */ HAL_UART_Receive_IT(huart2, voice_rx_byte, 1); } }这里有个容易出错的地方HAL_UART_Receive_IT在回调函数里必须重新调用否则只能接收一次。很多人忘了这一步结果语音模块只响应第一次指令后面就没反应了。5.3 指令解析与状态机设计收到语音模块的指令码之后STM32需要根据当前状态决定怎么响应。比如收到“现在几点”的指令码如果当前在正常显示模式就播报当前时间如果当前在设置模式就忽略这个指令。这就需要一个状态机来管理。我设计的状态机有三个状态正常显示状态、时间设置状态、日期设置状态。按键用来切换状态语音指令在正常显示状态下才响应。状态切换的时候OLED显示对应的提示信息比如进入设置状态后显示“设置年”然后按加/减键调整按确认键进入下一项。typedef enum { STATE_NORMAL, /* 正常显示 */ STATE_SET_YEAR, /* 设置年 */ STATE_SET_MONTH, /* 设置月 */ STATE_SET_DAY, /* 设置日 */ STATE_SET_HOUR, /* 设置时 */ STATE_SET_MINUTE, /* 设置分 */ STATE_SET_SECOND /* 设置秒 */ } SystemState; SystemState current_state STATE_NORMAL; void handle_voice_command(uint8_t cmd) { if (current_state ! STATE_NORMAL) { return; /* 设置状态下不响应语音指令 */ } switch (cmd) { case CMD_WHAT_TIME: voice_report_time(); break; case CMD_WHAT_DATE: voice_report_date(); break; case CMD_SET_TIME: current_state STATE_SET_YEAR; break; default: break; } }5.4 语音播报的实现方式语音播报有两种方案。一种是SU-03T本身支持TTS文本转语音你可以通过串口发送文本模块直接播报出来。另一种是提前录好语音片段存在模块里通过指令触发播放。SU-03T的TTS功能需要确认固件版本是否支持有些版本只支持词条识别不支持TTS。如果SU-03T不支持TTS可以用另一个方案在模块里预置几个语音文件比如“现在是十二点整”“今天是星期三”STM32根据当前时间选择对应的语音文件播放。但这种方式灵活性差时间变化多了语音文件不够用。所以最好还是选支持TTS的模块或者用带TTS功能的语音合成芯片。6. 按键交互与时间设置逻辑的完整实现6.1 按键消抖的两种方案对比按键消抖有硬件消抖和软件消抖两种。硬件消抖是在按键两端并联一个0.1uF电容成本低但效果一般。软件消抖是检测到按键按下后延时10到20毫秒再确认效果好但占用CPU时间。我一般用软件消抖配合定时器扫描的方式每10毫秒扫描一次按键状态连续三次读到相同状态才确认这样既不阻塞主循环消抖效果也好。/* 按键扫描函数每10ms调用一次 */ void key_scan(void) { static uint8_t key_cnt[3] {0}; uint8_t key_state[3]; key_state[0] HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0); key_state[1] HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1); key_state[2] HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_4); for (int i 0; i 3; i) { if (key_state[i] GPIO_PIN_RESET) { if (key_cnt[i] 3) key_cnt[i]; if (key_cnt[i] 3) { key_event(i); /* 触发按键事件 */ } } else { key_cnt[i] 0; } } }6.2 时间设置的状态流转时间设置的状态流转逻辑是这样的正常显示状态下按模式键进入设置年OLED显示“设置年”和当前年份按加键年份加一按减键年份减一再按模式键进入设置月以此类推直到设置完秒之后回到正常显示状态。在设置过程中如果超过10秒没有按键操作自动退出设置状态防止用户忘记退出导致时间一直不更新。设置完成之后把新的时间值通过HAL_RTC_SetTime和HAL_RTC_SetDate写回RTC。注意写RTC之前要先调用HAL_RTC_Init确保RTC处于可写状态写完再恢复正常。另外设置秒的时候建议把秒清零这样时间从整秒开始走看起来更整齐。6.3 设置过程中的显示刷新设置状态下OLED的显示内容和正常状态不一样需要单独设计。我的做法是设置状态下第一行显示当前设置项的名称比如“设置年”第二行显示当前值比如“2025”第三行显示操作提示“/-调整 模式键确认”。这样用户一看就知道在设置什么、怎么操作。刷新的时候要注意设置状态下每次按键都会改变显示内容所以每次按键事件后都要刷新对应的显示区域。但不要刷整屏只刷第二行的数值部分就行减少I2C通信量。7. 调试过程中最容易卡住的几个问题7.1 OLED不亮的排查链路OLED不亮是最常见的问题排查思路要按顺序来。第一步确认供电。用万用表量OLED的VCC和GND之间是不是3.3V如果电压不对检查STM32的3.3V输出是否正常。第二步确认I2C地址。写一个I2C扫描程序扫描0x00到0x7F的地址看哪个地址有应答。如果扫不到任何地址检查SCL和SDA有没有接反上拉电阻有没有接。第三步确认初始化序列。如果地址扫到了但屏幕还是不亮大概率是初始化序列有问题重点检查电荷泵那条命令0x8D, 0x14有没有发。我遇到过一种情况I2C地址扫到了初始化序列也发了但屏幕就是不亮。后来用逻辑分析仪抓I2C波形发现STM32发出的时钟频率太高了400kHz下OLED模块响应不过来。把I2C时钟降到100kHz之后就正常了。所以如果你遇到类似问题不妨试试降低I2C速度。7.2 语音模块无响应的常见原因语音模块没反应先检查供电。SU-03T的工作电压是5V如果你接的是3.3V模块可能不工作或者工作不稳定。然后检查串口波特率模块默认9600STM32这边也要设成9600。再检查TX和RX有没有接反模块的TX要接STM32的RX模块的RX要接STM32的TX交叉连接。如果以上都没问题用串口助手直接连模块对着模块说话看串口助手能不能收到数据。如果收不到说明模块本身没识别到语音可能是词条没配置好或者麦克风有问题。如果能收到数据但STM32没反应那就是STM32这边的串口接收配置有问题检查中断有没有使能、回调函数有没有重新开启接收。7.3 RTC时间不走或者走不准RTC时间不走首先检查LSE晶振有没有起振。用示波器量PC14和PC15引脚看有没有32.768kHz的正弦波。如果没有检查晶振有没有焊好、负载电容对不对。如果晶振没问题但时间还是不走检查RTC初始化代码里有没有使能LSE、有没有等待LSE就绪。时间走不准如果是每天差几秒那是晶振精度问题属于正常范围。如果每天差几分钟甚至几小时那可能是RTC时钟源选错了比如用了LSI内部RC振荡。检查RCC_OscInitStruct里RTC时钟源是不是RCC_RTCCLKSOURCE_LSE。7.4 程序下载失败的处理STM32下载失败通常有几个原因。一是BOOT引脚状态不对BOOT0要接低电平BOOT1随便。二是串口被占用了比如你用了USART1做调试输出下载的时候串口助手还开着就会冲突。三是芯片被读保护了需要先解除读保护再下载。四是下载工具配置不对比如FlyMcu的波特率设太高了改成115200或者9600试试。还有一种情况是芯片进入了低功耗模式下载器唤醒不了。这时候按住复位键点击下载然后松开复位键通常能下载进去。8. 从万年历延伸出去的扩展玩法这个项目做完之后框架已经搭好了往上加功能很方便。比如加一个DHT11温湿度传感器用单总线协议读取温湿度显示在OLED第三行。DHT11的驱动很简单关键是时序要准微秒级的延时用HAL_Delay不行得用定时器或者__NOP()循环来做。加BH1750光照传感器也很实用I2C接口和OLED共用一条总线地址不冲突就行。BH1750可以测环境光照强度根据光照自动调节OLED亮度晚上暗一点白天亮一点体验会好很多。如果想让这个项目更有实用价值可以加一个继电器控制输出设定时间到了自动打开或者关闭某个电器。比如早上7点自动开灯晚上11点自动关灯。继电器的控制很简单一个GPIO输出高低电平就行但要注意继电器是感性负载GPIO和继电器之间最好加一个光耦隔离保护STM32。语音方面还可以扩展更多指令比如“打开灯光”“关闭灯光”“温度多少”配合相应的传感器和执行器整个系统就变成了一个语音控制的智能家居终端。SU-03T支持上百条词条足够覆盖这些指令。我在实际做这个项目的过程中最大的体会是不要一上来就把所有功能都堆上去先把RTC和OLED跑通确认时间能正常显示再加语音模块最后加按键设置。每加一个模块就单独测试确认没问题再往下走。这样出问题的时候容易定位不会因为多个模块互相干扰而找不到原因。另外HAL库虽然方便但有些地方封装得太深出问题的时候还是要翻参考手册看寄存器别完全依赖库函数。