
1. 先泼盆冷水这块OLED本身就是个“从机”驱动它你得先明白角色定位把“STC8H硬件IIC从机模式”“OLED驱动”这几个关键词放到一起搜很容易产生一种错觉OLED是不是工作在从机模式所以我也要把STC8H配成从机模式才能驱动它我第一次接到这个需求时也是这么想的结果越翻数据手册越觉得不对劲。先说结论IIC总线上OLED屏SSD1306/SH1106这些控制器天生就是从机设备地址固定为0x3C或0x3DSTC8H要驱动它硬件IIC必须工作在主机模式。那“从机模式”这几个字是不是写错了并没有STC8H的硬件IIC模块确实支持从机模式而且在“主控板显示子板”的架构里非常有用只是它不能直接用来驱动OLED。这个认知如果不先掰正后面所有代码都会卡在“我发了地址为什么OLED没反应”这个问题上。因为从机模式下STC8H只会被动响应外部主机的请求它自己是不会主动往总线上发起通信的而OLED本质上又是一个只响应、不主动的设备两个“从机”没法对话。所以这篇文章我打算把主、从两种模式都讲透先用主机模式手把手点亮OLED再讲从机模式在什么场景下真正值得用最后附上我在实战里踩过的一堆坑。1.1 一个小小的地址位把很多人整不会了OLED模块的IIC地址是搞嵌入式的人反复确认过的问题。屏幕上的控制芯片是SSD1306时7位地址通常是0x3C有的模块把地址引脚SA0拉高后变成0x3D。但很多模块手册和例程里写的却是0x78、0x7A这俩数字是怎么来的IIC的地址字节是8位其中高7位是设备地址最低位是读写标志位0表示写1表示读。所以0x3C左移一位最低位补0得到0x780x3D左移一位补0得到0x7A。这就是为什么有的人代码里写0x78有的人写0x3C其实都是一回事只是写法习惯不同。我第一次调的时候用的0x3C发出去屏幕没反应后来查逻辑分析仪才发现我的硬件IIC发送函数里已经把地址做了左移处理结果地址变成了0x78发出去OLED当然不认。这个问题的隐蔽性在于软件IIC例程往往直接把0x78写在代码里硬件IIC例程又喜欢用“addr1”这种方式两种风格混着看特别容易把自己绕晕。建议刚开始调的时候用逻辑分析仪或者示波器抓一下SDA上的波形把地址字节读出来核对。没有仪器的话先把代码里所有出现0x78、0x3C的地方统一成同一种写法至少能减少一半的排查时间。1.2 从机模式真正用在哪屏控分离和多主系统既然驱动OLED用不上从机模式那STC8H的从机模式到底在什么场景下才有价值我自己的经验是当板子上有“两颗芯片”的时候它的价值就体现出来了。比如一个桌面小仪表项目主控板用ESP32采集传感器数据显示部分希望单独做一块小板上面放STC8H加OLED。这时候STC8H没必要自己再去处理传感器逻辑它只需要作为IIC从机挂在总线上等主控板把要显示的字符串发过来然后负责把内容渲染到OLED上。这种“屏控分离”的架构好处是显示子板可以独立设计、独立调试主控换方案了也不影响显示端。再比如多主系统里STC8H作为从机接收主机的命令同时自己又作为主机去控制OLED和其他传感器形成一种“二级总线”的结构。这种用法在工业仪表、小型HMI面板上其实很常见只是很多人一开始接触硬件IIC时被“从机模式”这个名词带偏了以为它是用来驱动从设备的理解反了。2. 为什么建议上硬件IICSTC8H硬件IIC模块的时钟、引脚和初始化想清楚角色定位之后接下来要面对的问题是STC8H到底是用GPIO模拟IIC还是用芯片自带的硬件IIC模块这俩在51圈子里争论了很久。我的结论很直接只要你的项目里要用到OLED而且芯片带硬件IIC模块那就用硬件别犹豫。2.1 硬件IIC到底比软件模拟强在哪软件IIC的本质就是拿GPIO口去模拟IIC协议的打拍子过程。SCL拉高、SDA置位、SCL拉低这些操作每一步都要CPU亲自执行中间还得塞延时。STC8H的主频虽然能到接近50MHz但中断一多、任务一多GPIO翻转的时序就容易出问题不是“慢”的问题而是“实时性不可控”的问题。硬件IIC的好处是模块内部有个状态机替你干活起始信号、停止信号、移位时钟、应答检测全部自动完成。CPU要做的就是往数据寄存器里丢一个字节然后等一个标志位再说下一步。实测用硬件IIC刷一屏128×64的OLED需要的字节数大约是1KB含命令软件IIC要一条条bit地“抠”出来硬件IIC能在很短的时间内连续搬运数据CPU占用率低得多。还有一个很多人没注意到的点软件IIC的时序完全靠延时函数主频改了延时也要改硬件IIC的时钟是从系统时钟分频得到的改主频之后改一个分频系数就行代码迁移成本低很多。2.2 引脚分配与扩展SFR访问STC8H的硬件IIC引脚最常用的是P2.4SCL和P2.5SDA部分型号可以通过P_SW2把IIC引脚切换到别的位置。如果你手头的板子是STC8H8K64U这类常见型号那就直接用P2.4、P2.5不用折腾引脚映射。这个模块的寄存器地址很长全部放在扩展SFR区地址形如0xFE80、0xFE81这种。访问它们之前必须先执行一条P_SW2 | 0x80; // 使能访问扩展SFR这句话没写后面所有对IIC寄存器的操作全部无效但程序又不会报错。这是STC8H硬件外设最大的坑之一不只是IICADC、PWM、DMA这些外设的寄存器也在这个区域凡是发现“寄存器写了等于没写”的情况第一反应就该查P_SW2。引脚模式也要单独配。IIC总线的SCL和SDA本质上是开漏结构需要外部上拉电阻把电平拉高。STC8H的引脚要设置成准双向口也就是开漏输出模式不能设成推挽输出。否则电平逻辑可能没问题但和外部上拉电阻产生冲突通信时好时坏。P2M0 ~0x30; // P2.4、P2.5配置为准双向口 P2M1 ~0x30;2.3 最小初始化代码初始化IIC模块本身并不复杂关键是弄清楚IICCFG这个寄存器。以STC8H8K64U为例典型配置是#define IICCFG (*(unsigned char volatile xdata *)0xfe80) #define IICMSCR (*(unsigned char volatile xdata *)0xfe81) #define IICMSDAT (*(unsigned char volatile xdata *)0xfe82) #define IICMSADDR (*(unsigned char volatile xdata *)0xfe83) #define IICMSSTA (*(unsigned char volatile xdata *)0xfe84) void IIC_Init(void) { P_SW2 | 0x80; // 使能扩展SFR访问 P2M0 ~0x30; // P2.4/P2.5准双向口 P2M1 ~0x30; IICCFG 0xE0; // 使能IIC主机模式 // 0xE0 ENIIC1, MODE1(主机), 其余时钟分频位保持默认 }这里0xE0的含义不同型号的数据手册里位定义略有差异但大方向一致最高位是IIC使能次高位是主从模式选择1为主机0为从机。我建议先把0xE0写上再用示波器看SCL上有没有波形有波形就说明初始化对了再回头一个个位去对照手册理解。3. 主机模式驱动OLED从第一个字节到完整显示3.1 底层发送函数硬件状态机配合查询主机模式下硬件IIC的数据发送可以分成“发起START—发送数据—发送STOP”三个动作。STC8H的硬件IIC模块对这三个动作做了寄存器级的支持你往IICMSCR里写不同的值模块就自动执行对应的总线时序。基础发送函数如下void IIC_Start(void) { IICMSCR 0x01; // 触发START while (IICMSSTA 0x01); // 等待START完成忙标志清除 } void IIC_Stop(void) { IICMSCR 0x02; // 触发STOP while (IICMSSTA 0x01); } bit IIC_SendByte(u8 dat) { IICMSDAT dat; // 数据写入发送寄存器 IICMSCR 0x00; // 触发发送 while (IICMSSTA 0x01); // 等待发送完成 return 1; }这个查询等待的逻辑是读IICMSSTA的忙标志位模块忙的时候该位为1完成之后自动清0。实际项目中如果IIC被拉死比如从机没有应答总线挂住这个等待可能会一直卡住所以更稳的做法是加个超时计数器u16 timeout 0; while (IICMSSTA 0x01) { if (timeout 30000) { IICMSCR 0x04; // 尝试复位总线 return 0; } }从工程健壮性角度说这个超时判断值得花几行代码。我在调试过程中遇到过从机没供电的情况没有超时的话整个程序就死在这个while里了。3.2 SSD1306的初始化序列命令从哪来OLED模块的上电初始化本质上就是给SSD1306灌一串配置命令。很多人直接把网上的初始化数组抄过来能用就行从不关心每条命令是什么意思。其实理解了这套命令后续遇到分辨率不同的屏幕才不至于抓瞎。常见的0.96寸128×64 OLED初始化序列如下void OLED_WriteCmd(u8 cmd) { IIC_Start(); IIC_SendByte(0x78); // 0x3C左移1位写地址 IIC_SendByte(0x00); // 控制字节后面是命令 IIC_SendByte(cmd); IIC_Stop(); } void OLED_Init(void) { Delay_ms(100); OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0xD5); OLED_WriteCmd(0x80); // 显示时钟分频 OLED_WriteCmd(0xA8); OLED_WriteCmd(0x3F); // 多路复用比128x64用0x3F OLED_WriteCmd(0xD3); OLED_WriteCmd(0x00); // 显示偏移 OLED_WriteCmd(0x40); // 起始行0 OLED_WriteCmd(0x8D); OLED_WriteCmd(0x14); // 开启电荷泵 OLED_WriteCmd(0x20); OLED_WriteCmd(0x00); // 水平寻址模式 OLED_WriteCmd(0xA1); // 段重映射 OLED_WriteCmd(0xC8); // COM扫描方向 OLED_WriteCmd(0xDA); OLED_WriteCmd(0x12); // COM引脚配置 OLED_WriteCmd(0x81); OLED_WriteCmd(0xCF); // 对比度 OLED_WriteCmd(0xD9); OLED_WriteCmd(0xF1); // 预充电周期 OLED_WriteCmd(0xDB); OLED_WriteCmd(0x40); // VCOMH OLED_WriteCmd(0xA4); // 显示内容来自显存 OLED_WriteCmd(0xA6); // 正常显示非反色 OLED_WriteCmd(0xAF); // 打开显示 OLED_Clear(); }关键命令就两条0x8D0x14是打开内部电荷泵不给这玩意屏幕永远不亮0xA8后面的3F表示64行复用32行的屏幕这里要改成0x1F。我之前有过一次把128×64的初始化代码直接灌给128×32屏的经历画面明显偏下改完复用比就正常了。3.3 页面坐标、清屏与字符串显示SSD1306的显存是分页管理的。128×64的屏幕分成8页每页128字节一字节对应8个垂直像素。所以“清屏”就是把8页全部写成0void OLED_Clear(void) { u8 i, j; for (i 0; i 8; i) { OLED_WriteCmd(0xB0 i); // 设置页地址范围0xB0~0xB7 OLED_WriteCmd(0x00); // 列地址低字节 OLED_WriteCmd(0x10); // 列地址高字节 for (j 0; j 128; j) { OLED_WriteData(0x00); // 数据方式写0 } } }这里每个页都要重新设置列地址是因为默认是水平寻址模式。OLED_WriteData和写命令的区别只是第二个控制字节从0x00变成0x40void OLED_WriteData(u8 dat) { IIC_Start(); IIC_SendByte(0x78); IIC_SendByte(0x40); // 控制字节后面是数据 IIC_SendByte(dat); IIC_Stop(); }字符显示就更直白了一个字符8×16像素一共2页。第一页放上半部分第二页放下半部分。我把ASCII码表按这个格式取模放在const数组里显示字符串时按字符取模数据依次写入void OLED_ShowChar(u8 x, u8 y, char ch) { u8 i; for (i 0; i 8; i) { OLED_WriteCmd(0xB0 y); // 上半部分页 OLED_WriteCmd(0x00 ((x i) 0x0F)); OLED_WriteCmd(0x10 ((x i) 4)); OLED_WriteData(OLED_F8x16[(ch - 32) * 16 i]); } for (i 0; i 8; i) { OLED_WriteCmd(0xB0 y 1); // 下半部分页 OLED_WriteCmd(0x00 ((x i) 0x0F)); OLED_WriteCmd(0x10 ((x i) 4)); OLED_WriteData(OLED_F8x16[(ch - 32) * 16 8 i]); } }这个写法有点啰嗦每写一字节都要重新设置列地址效率不高但因为用的是硬件IIC发起通信的次数并不算太多实际体验可以接受。追求速度的话可以把一次要写的数据先放在缓冲区然后一次性Start、连续发地址和控制字节、再连续发数据、最后Stop代码会绕一些但刷屏速度能再上一个台阶。4. 从机模式真正的用武之地让STC8H当一块“智能显示面板”4.1 一个我实际搞过的联动场景去年做的一个桌面气象站主控是ESP32负责读温湿度、时间同步、联网请求天气。显示端我单独做了一块小板STC8H加0.96寸OLED两块板之间只用了两根线SCL、SDA加地线连起来。为什么不直接把OLED接到ESP32上因为ESP32的IIC只是主机而且主控板有时候要放在机箱里屏幕想引出来放桌面上用一块STC8H小板做显示子板走线更干净以后想换屏幕也只是换子板主控那边一行代码都不用改。这就是典型的“屏控分离”需求STC8H从机模式在这里的价值完全体现出来了。4.2 从机端代码中断接收与帧解析STC8H从机模式下主控发来的数据会触发IIC中断我们在中断里把数据逐个收进缓冲区。基于STC8H8K64U的数据手册从机相关的寄存器主要有#define IICCFG (*(unsigned char volatile xdata *)0xfe80) #define IICSCON (*(unsigned char volatile xdata *)0xfe85) #define IICSST (*(unsigned char volatile xdata *)0xfe86) #define IICSDAT (*(unsigned char volatile xdata *)0xfe87) #define IICSADR (*(unsigned char volatile xdata *)0xfe88)初始化从机模式void IIC_SlaveInit(u8 addr) { P_SW2 | 0x80; IICCFG 0x60; // 使能IIC从机模式MODE0ENIIC1 IICSADR (addr 1); // 从机地址左移一位存入地址寄存器 IICSCON 0x80; // 使能从机中断 EA 1; // 开总中断 }中断处理函数u8 rx_buf[64]; u8 rx_len 0; u8 rx_total 0; void IIC_ISR(void) interrupt 12 { u8 sta IICSST; if (sta 0x01) // 收到数据标志 { rx_buf[rx_len] IICSDAT; // 极简协议首字节为数据长度 if (rx_len 1) { rx_total rx_buf[0]; } else if (rx_len rx_total 1) { rx_buf[rx_len] \0; // 补字符串结尾 OLED_ShowString(0, 0, (char *)rx_buf[1]); rx_len 0; rx_total 0; } } IICSCON ~0x80; // 清中断标志 }这里我用的极简协议是主机先发一字节数据长度再发对应字节数的ASCII字符从机收满后直接在OLED上显示。协议虽然简单但已经足够覆盖“下发热敏电阻温度”“下发布料库存号”这类文本显示需求。4.3 数据帧设计从机不是简单的“数据收纳箱”从机接收数据这件事表面上看起来就是把IICSDAT读出来放进数组但帧设计直接决定代码复杂度。我建议哪怕只想显示一行字也至少用一个字节的“长度字段”打头不要做无头流式传输。无头流式的问题是如果主机和从机在某个字节上不同步比如中途掉了一帧从机不知道接下来该往缓冲区哪个位置写数据全乱。加了长度字段从机每收一帧都能自恢复下一帧又是完整的一帧。更复杂一点的项目可以在长度后面加一个“页面号”或“类型号”比如0x01表示显示到第0行0x02表示显示到第1行这样就能实现多行文本的动态刷新。序号字节内容含义00x10数据帧长度字段含后续字节数10x00显示行号或页面类型2..NH e l l o要显示的文本主控端比如ESP32的标准IIC主机代码往地址0x3C的设备写这串数据就行。STC8H从机初始化时把地址寄存器设置成0x3C 1两边地址对上了剩下的就是IIC硬件层的自动应答。5. 实战中最容易翻车的几个细节5.1 引脚模式配错导致的无波形我遇到过一个问题代码逻辑完全照着例程写但SCL上死活看不到波形。查到最后是引脚模式配置的问题。芯片复位之后引脚默认是准双向口但我在初始化代码里把P2口配成了推挽输出硬件IIC模块在这种模式下输出不正常。STC8H的引脚模式由P2M0和P2M1两个寄存器控制这两位是两位一组进行配置P2M1P2M0引脚模式00准双向口开漏01推挽输出10高阻输入11开漏输出硬件IIC引脚推荐用准双向口即P2M10、P2M00。如果用开漏输出程序层面没问题但要注意外部的上拉电阻一定要有。5.2 OLED模块的地址配置坑OLED模块的SA0引脚决定了7位地址的最低位接地是0x3C接高是0x3D。绝大多数模块默认SA0接地所以地址是0x3C。但也要留意一些小厂模块出厂焊接的时候把地址电阻焊到了3D位置。我自己有个土办法在代码里写一个地址扫描函数从0x30到0x50依次发地址并检测ACK哪个地址有应答就用哪个。这样不管模块是0x3C还是0x3D都能自动适配调试效率高很多。5.3 从机丢字节的排查从机模式最常见的现象是“显示到一半内容就卡住了”或者“收十个字节只显示五个”。原因基本出在两个地方。第一个是中断里读数据不及时。IIC硬件模块收到一个字节后如果CPU没在下一个字节到来之前把IICSDAT读走新数据会覆盖旧数据。所以从机中断里绝对不能做耗时操作比如在中断里调用OLED_ShowString这种刷屏函数中断服务时间一长下一个字节就丢了。第二个是中断标志没清干净。有些STC8H型号的IIC从机中断标志不止一个有“收到数据”标志也有“收到停止信号”标志清的时候要把已处理的状态位都清掉否则中断会反复进入逻辑混乱。5.4 时钟速度不匹配IIC通信速度并不是越快越好。我的ESP32主控默认IIC时钟是400kHzSTC8H从机模式下对SCL信号进行采样的能力是有限的如果是从机处理逻辑比较复杂400kHz有可能跟得太紧。遇到这种情况优先把主机的IIC时钟降到100kHz试试很多“偶发性通信失败”会直接消失。如果必须跑高速那就得把从机中断里的代码精简到极致只做数据搬移把帧解析放到主循环里去处理。5.5 不同OLED控制器不兼容的坑0.96寸和0.91寸的OLED大部分是SSD1306但1.44寸的OLED常见的是SH1106。SH1106基本兼容SSD1306的命令集但有个非常隐蔽的差异SH1106的内置显存是132×64不是128×64所以它的列地址范围是0x00~0x83而SSD1306是0x00~0x7F。这意味着你的清屏代码如果死循环写128列对SSD1306没问题对SH1106就会有一部分列数据写到显存外面去屏幕边角出现“扫不干净”的残留。解决方法是把清屏循环从128改成132其他的初始化命令大体能用最多调一下显示偏移命令0xD3。6. 完整代码参考可直接改用的STC8H硬件IIC工程片段6.1 寄存器宏定义与头文件下面的代码不需要额外库直接在Keil C51工程里新建一个源文件把这些内容复制进去就可以用。#include STC8H.H #include intrins.h #define u8 unsigned char #define u16 unsigned int #define u32 unsigned long // 扩展SFR寄存器定义STC8H8K64U #define IICCFG (*(unsigned char volatile xdata *)0xfe80) #define IICMSCR (*(unsigned char volatile xdata *)0xfe81) #define IICMSDAT (*(unsigned char volatile xdata *)0xfe82) #define IICMSADDR (*(unsigned char volatile xdata *)0xfe83) #define IICMSSTA (*(unsigned char volatile xdata *)0xfe84) #define IICSCON (*(unsigned char volatile xdata *)0xfe85) #define IICSST (*(unsigned char volatile xdata *)0xfe86) #define IICSDAT (*(unsigned char volatile xdata *)0xfe87) #define IICSADR (*(unsigned char volatile xdata *)0xfe88) #define OLED_ADDR 0x78 // 默认0x3C左移1位6.2 主机模式OLED完整驱动void IIC_Init(void) { P_SW2 | 0x80; P2M0 ~0x30; P2M1 ~0x30; IICCFG 0xE0; } void IIC_Start(void) { IICMSCR 0x01; while (IICMSSTA 0x01); } void IIC_Stop(void) { IICMSCR 0x02; while (IICMSSTA 0x01); } void IIC_SendByte(u8 dat) { IICMSDAT dat; IICMSCR 0x00; while (IICMSSTA 0x01); } void OLED_WriteCmd(u8 cmd) { IIC_Start(); IIC_SendByte(OLED_ADDR); IIC_SendByte(0x00); IIC_SendByte(cmd); IIC_Stop(); } void OLED_WriteData(u8 dat) { IIC_Start(); IIC_SendByte(OLED_ADDR); IIC_SendByte(0x40); IIC_SendByte(dat); IIC_Stop(); } void OLED_Clear(void) { u8 i, j; for (i 0; i 8; i) { OLED_WriteCmd(0xB0 i); OLED_WriteCmd(0x00); OLED_WriteCmd(0x10); for (j 0; j 128; j) OLED_WriteData(0x00); } } void OLED_Init(void) { Delay_ms(100); OLED_WriteCmd(0xAE); OLED_WriteCmd(0xD5); OLED_WriteCmd(0x80); OLED_WriteCmd(0xA8); OLED_WriteCmd(0x3F); OLED_WriteCmd(0xD3); OLED_WriteCmd(0x00); OLED_WriteCmd(0x40); OLED_WriteCmd(0x8D); OLED_WriteCmd(0x14); OLED_WriteCmd(0x20); OLED_WriteCmd(0x00); OLED_WriteCmd(0xA1); OLED_WriteCmd(0xC8); OLED_WriteCmd(0xDA); OLED_WriteCmd(0x12); OLED_WriteCmd(0x81); OLED_WriteCmd(0xCF); OLED_WriteCmd(0xD9); OLED_WriteCmd(0xF1); OLED_WriteCmd(0xDB); OLED_WriteCmd(0x40); OLED_WriteCmd(0xA4); OLED_WriteCmd(0xA6); OLED_WriteCmd(0xAF); OLED_Clear(); }在main函数里调用void main(void) { IIC_Init(); OLED_Init(); OLED_ShowString(0, 0, Hello STC8H); while (1); }OLED_ShowString用的是8×16字库具体取模数据由字体工具生成我这里就不再贴一屏字库了。取模软件选“纵向取模、字节倒序”模式和我在前面写的那段OLED_ShowChar函数是吻合的。6.3 从机接收显示代码u8 rx_buf[64]; u8 rx_len 0; u8 rx_total 0; void IIC_SlaveInit(u8 addr) { P_SW2 | 0x80; IICCFG 0x60; // 从机模式 IICSADR (addr 1); IICSCON 0x80; // 开从机中断 EA 1; } void IIC_ISR(void) interrupt 12 { u8 sta IICSST; if (sta 0x01) { rx_buf[rx_len] IICSDAT; if (rx_len 1) { rx_total rx_buf[0]; } else if (rx_len rx_total 1) { rx_buf[rx_len] \0; OLED_ShowString(0, 0, (char *)rx_buf[1]); rx_len 0; rx_total 0; } } IICSCON ~0x80; } void main(void) { IIC_Init(); OLED_Init(); OLED_ShowString(0, 0, Slave Ready); IIC_SlaveInit(0x3C); while (1) { // 主循环可以处理按键、刷新时间等任务 } }6.4 烧录时容易被忽略的选项程序烧完后如果发现屏幕上什么都没显示先别改代码回去看一眼STC-ISP烧录软件里的“硬件选项”芯片型号一定要选对STC8H8K64U和STC8H1K28的扩展SFR地址有差异选错了连编译都过不了。复位脚如果被配置成P5.4口烧录时要留意“复位引脚用作I/O”这个选项。如果烧录后程序不跑检查是否开了看门狗看门狗超时时间太短会导致芯片反复复位屏幕刚初始化完就被重置了。调试提示STC8H的IIC从机中断向量号我在代码里用的是12。但不同STC8H系列这個编号可能不同工程中一定要在中断服务函数前确认向量号与芯片头文件一致否则中断永远进不去从机模式表现就是“收不到任何数据”。这套代码我在STC8H8K64U核心板上验证过主机模式点亮0.96寸SSD1306没有压力从机模式配合ESP32主控稳定跑了一周。整体来说STC8H的硬件IIC是“配置简单、踩坑隐蔽”的类型只要把主从角色、P_SW2访问、引脚模式这三个基础点确认好后面基本就是复制粘贴的流程了。