1. 系统架构与数据流向解析
在嵌入式物联网应用中,将STM32采集的温度数据实时呈现于手机APP,本质上是一个典型的端-云-端三级数据链路工程。该架构并非简单的串口直连或蓝牙透传,而是依托成熟的公有云平台能力,构建具备设备管理、数据路由、安全认证和跨平台分发能力的工业级数据通路。整个系统由四个核心实体构成:STM32传感器节点、ESP8266 Wi-Fi通信模块、阿里云IoT平台、以及Android/iOS客户端APP。它们之间通过明确的职责划分与标准化协议协同工作,形成一个可扩展、可运维、可追溯的数据闭环。
STM32作为边缘感知节点,承担着原始物理量采集与本地预处理任务。本项目中,温度传感器(如DS18B20或NTC+ADC)连接至STM32的GPIO或ADC外设,通过HAL库完成采样、滤波与单位换算,最终生成带时间戳的摄氏度数值。该数值不直接暴露给网络,而是作为待上传的有效载荷(payload),交由通信层处理。
ESP8266在此架构中扮演“边缘网关”的角色,其本质是运行AT固件的独立Wi-Fi SoC。它不参与温度算法,也不解析业务逻辑,仅负责建立TLS加密隧道、执行MQTT协议栈、完成设备身份认证,并将STM32发来的JSON格式数据包,按照MQTT Topic规则发布至阿里云指定的上行主题。这种软硬件解耦设计极大降低了主控MCU的资源压力——STM32无需集成TCP/IP协议栈,无需管理Wi-Fi连接状态,更无需处理证书验证等复杂安全流程,所有网络层负担均由ESP8266承担。
阿里云IoT平台是整个系统的中枢神经。它首先通过三元组(ProductKey、DeviceName、DeviceSecret)对设备进行双向认证,确保接入合法性;其次,依据物模型(Thing Model)定义的数据结构,对上行消息进行格式校验与语义解析;最后,通过规则引擎将清洗后的温度数据路由至两个出口:一是写入时序数据库供Web控制台可视化,二是触发云产品流转,将数据推送给已订阅的移动APP。这一过程完全屏蔽了底层网络细节,开发者只需关注业务数据本身。
手机APP则是最终的数据消费端。它不直接连接STM32或ESP8266,而是通过阿里云提供的移动端SDK(如AliyunIoTSDK for Android),以长连接方式接入IoT平台的消息总线。当云端检测到某设备的温度属性发生变更时,会主动向该设备绑定的所有APP实例推送增量更新,实现毫秒级数据同步。这种“云推送”模式彻底规避了APP轮询带来的电量与流量浪费,也解决了NAT穿透、IP动态变化等传统直连方案的顽疾。
理解这一分层架构至关重要。它决定了后续所有配置的逻辑起点:STM32的代码只需专注“把数据准备好”,ESP8266的配置只需确保“能连上云”,而云端与APP的工作则属于平台服务范畴。任何试图绕过云平台、让APP直连ESP8266的尝试,都会在设备数量增长、网络环境复杂化后迅速暴露出可维护性差、安全性弱、扩展性低等致命缺陷。
2. STM32端温感采集与数据封装实现
STM32的数据采集与封装环节,是整个链路的源头,其质量直接决定后续所有环节的可靠性。本项目采用HAL库开发,以STM32F103C8T6(主流入门型号)为例,假设使用内部温度传感器(TS)作为数据源——此选择具有零外围器件、高复用性的优势,但需注意其±1.5℃的典型精度,适用于环境监测等非精密场景。
2.1 温度传感器初始化与校准
内部温度传感器并非独立外设,而是集成在ADC1的通道16(ADC_Channel_16)。因此,其初始化必须嵌入ADC整体配置流程:
// 启用ADC1与相关时钟 __HAL_RCC_ADC1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); // 配置ADC1为连续扫描模式,单次转换 ADC_HandleTypeDef hadc1; hadc1.Instance = ADC1; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.ScanConvMode = DISABLE; // 内部传感器单通道,禁用扫描 hadc1.Init.ContinuousConvMode = ENABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; if (HAL_ADC_Init(&hadc1) != HAL_OK) { Error_Handler(); } // 配置ADC通道16(内部温度传感器) ADC_ChannelConfTypeDef sConfig; sConfig.Channel = ADC_CHANNEL_TEMPSENSOR; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; // 最长采样时间,提升精度 if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) { Error_Handler(); } // 必须启用温度传感器使能位(关键!) ADC->CR2 |= ADC_CR2_TSVREFE;此处ADC_CR2_TSVREFE位的设置是常被忽略的关键步骤。若未置位,ADC读取通道16将始终返回0。同时,采样时间选择239.5个ADC周期,是因为温度传感器输出阻抗较高,需要更长的采集窗口以确保电容充分充电,避免读数偏低。
2.2 温度值计算与软件校准
HAL库提供的HAL_ADC_GetValue()返回的是12位原始数字量(0–4095)。需将其转换为摄氏度,公式为:
Temperature(°C) = (V25 - VSENSE) / Avg_Slope + 25其中,V25是25℃时的参考电压(典型值1.43V),Avg_Slope是平均斜率(典型值4.3mV/℃)。这些参数存储在芯片的系统存储器中(地址0x1FFFF7E8起),需通过*(uint16_t*)强制类型转换读取:
// 从系统存储器读取校准参数 uint16_t *temp_cal_vrefint = (uint16_t*)0x1FFFF7BA; // VREFINT校准值 @30℃ uint16_t *temp_cal_temp = (uint16_t*)0x1FFFF7E8; // TS校准值 @25℃ // 启动ADC并获取转换值 HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY); uint32_t adc_value = HAL_ADC_GetValue(&hadc1); // 计算实际温度(简化版,忽略VREFINT波动) float temperature = ((float)(*(temp_cal_temp)) - (float)adc_value) * (3.3f / 4096.0f) / 0.0043f + 25.0f;实践中发现,出厂校准值存在批次差异。更鲁棒的做法是在设备首次启动时,于恒温环境中(如25℃水浴)手动记录ADC读数,计算出当前芯片的实际斜率与偏移,存入EEPROM或Flash备用区。这一步虽增加初始配置成本,却能将实测误差从±2℃压缩至±0.5℃以内。
2.3 数据封装为标准JSON格式
阿里云IoT平台要求上行数据必须符合其物模型定义,通常采用JSON格式。为降低STM32内存压力,应避免使用通用JSON库(如cJSON),转而采用轻量级字符串拼接。假设物模型中温度属性名为temperature,单位为℃,则封装函数如下:
#define MAX_JSON_LEN 128 char json_buffer[MAX_JSON_LEN]; void build_temperature_json(float temp_c) { int len = snprintf(json_buffer, MAX_JSON_LEN, "{\"id\":\"%lu\",\"version\":\"1.0\",\"params\":{\"temperature\":%.2f}}", HAL_GetTick(), temp_c); if (len < 0 || len >= MAX_JSON_LEN) { // 缓冲区溢出处理 json_buffer[0] = '\0'; return; } }HAL_GetTick()生成唯一消息ID,用于云端去重与调试追踪;version字段标识物模型版本;params对象内严格匹配云端定义的属性名。此格式可直接作为MQTT payload发送,无需额外解析。
3. ESP8266 AT指令交互与MQTT连接建立
ESP8266作为通信桥梁,其稳定性是系统可用性的生命线。本节基于官方AT固件(v2.2.0及以上),通过UART与STM32通信,所有操作均通过标准AT指令完成。关键在于理解AT指令的异步响应机制与状态机管理,而非简单地“发送-等待”。
3.1 UART接口配置与AT指令收发框架
STM32与ESP8266通过USART2连接(PA2-TX, PA3-RX),波特率固定为115200(AT固件默认)。为应对AT指令响应延迟,必须实现带超时的接收缓冲区:
#define AT_RX_BUFFER_SIZE 256 uint8_t at_rx_buffer[AT_RX_BUFFER_SIZE]; uint16_t at_rx_index = 0; // 重写HAL_UART_RxCpltCallback,实现环形接收 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART2) { // 将接收到的字节存入缓冲区 if (at_rx_index < AT_RX_BUFFER_SIZE - 1) { at_rx_buffer[at_rx_index++] = rx_byte; } HAL_UART_Receive_IT(&huart2, &rx_byte, 1); // 继续接收 } } // 轮询检查是否收到完整响应(含"\r\nOK\r\n"或"\r\nERROR\r\n") HAL_StatusTypeDef wait_for_at_response(const char* expected, uint32_t timeout_ms) { uint32_t start_tick = HAL_GetTick(); while (HAL_GetTick() - start_tick < timeout_ms) { if (at_rx_index > 0) { // 在缓冲区中搜索expected字符串 if (strstr((char*)at_rx_buffer, expected) != NULL) { // 清空缓冲区,准备下一次接收 memset(at_rx_buffer, 0, sizeof(at_rx_buffer)); at_rx_index = 0; return HAL_OK; } } HAL_Delay(10); } return HAL_TIMEOUT; }此框架避免了阻塞式HAL_UART_Receive导致的系统僵死,允许在等待AT响应的同时处理其他任务(如继续采集温度)。
3.2 连接阿里云的四步AT指令序列
建立MQTT连接需严格遵循以下顺序,任何步骤失败都必须回退重试:
第一步:复位并检查AT固件版本
// 发送:AT+RST // 期望响应:"ready" HAL_UART_Transmit(&huart2, (uint8_t*)"AT+RST\r\n", 8, HAL_MAX_DELAY); wait_for_at_response("ready", 5000);第二步:配置Wi-Fi STA模式并连接路由器
// 发送:AT+CWMODE=1 (仅STA模式) HAL_UART_Transmit(&huart2, (uint8_t*)"AT+CWMODE=1\r\n", 13, HAL_MAX_DELAY); wait_for_at_response("OK", 1000); // 发送:AT+CWJAP="SSID","PASSWORD" (替换为实际值) HAL_UART_Transmit(&huart2, (uint8_t*)"AT+CWJAP=\"MyWiFi\",\"12345678\"\r\n", 32, HAL_MAX_DELAY); wait_for_at_response("WIFI GOT IP", 10000); // 等待DHCP获取IP第三步:配置MQTT客户端参数
// 发送:AT+MQTTUSERCFG=0,1,"productKey|deviceName|deviceSecret","|",0,0,"" // 注:阿里云MQTT用户名为 "deviceName|productKey|securemode|signmethod|timestamp|pubkey" // 密码为 sign = hmacsha1(deviceSecret, content),content = "clientIddeviceName|productKey|securemode|signmethod|timestamp" // 此处需在STM32端预先计算签名,不能依赖ESP8266 HAL_UART_Transmit(&huart2, (uint8_t*)"AT+MQTTUSERCFG=0,1,\"a1B2c3D4e5|my_device|...\",\"\",0,0,\"\"\r\n", 60, HAL_MAX_DELAY); wait_for_at_response("OK", 1000);第四步:建立MQTT连接并订阅下行主题
// 发送:AT+MQTTCONN=0,"a1B2c3D4e5.iot-as-mqtt.cn-shanghai.aliyuncs.com",1883,1 HAL_UART_Transmit(&huart2, (uint8_t*)"AT+MQTTCONN=0,\"a1B2c3D4e5.iot-as-mqtt.cn-shanghai.aliyuncs.com\",1883,1\r\n", 68, HAL_MAX_DELAY); wait_for_at_response("CONNECTED", 10000); // 发送:AT+MQTTSUB=0,"/a1B2c3D4e5/my_device/user/get",1 (订阅APP下发指令的主题) HAL_UART_Transmit(&huart2, (uint8_t*)"AT+MQTTSUB=0,\"/a1B2c3D4e5/my_device/user/get\",1\r\n", 45, HAL_MAX_DELAY); wait_for_at_response("SUBACK", 2000);整个过程中,AT+MQTTUSERCFG的密码计算是最大难点。阿里云要求使用HMAC-SHA1算法,以deviceSecret为密钥,对content字符串签名。由于ESP8266 AT固件不提供此功能,必须在STM32端实现。可选用轻量级mbed TLS库或自行移植SHA1算法,输入content(如"clientIdmy_device|a1B2c3D4e5|1|hmacsha1|1609459200|")与deviceSecret,输出Base64编码的签名字符串。
4. 阿里云IoT平台物模型与Topic配置
阿里云IoT平台的物模型(Thing Model)是连接物理世界与数字世界的语义桥梁。它定义了设备的能力描述,包括属性(Properties)、服务(Services)、事件(Events)三大要素。本项目仅需配置一个温度属性,但其正确性直接决定了APP能否正确解析数据。
4.1 物模型属性定义规范
登录阿里云IoT控制台,在对应产品下进入“物模型”页签,点击“添加功能”。关键配置项如下:
| 字段 | 值 | 说明 |
|---|---|---|
| 功能名称 | temperature | 必须与STM32 JSON中params.temperature完全一致,区分大小写 |
| 标识符 | temperature | 系统自动生成,不可修改,即Topic中使用的字段名 |
| 数据类型 | float | 对应JSON中的浮点数值 |
| 单位 | ℃ | 显示用,不影响传输 |
| 读写类型 | 只读(Read-Only) | 温度由设备上报,APP不可写入 |
| 取值范围 | -40 ~ 85 | 符合常见环境温度区间,超出范围云端将拒绝接收 |
完成定义后,平台会自动生成该产品的物模型TSL(Thing Specification Language),以JSON Schema形式描述。开发者可下载此文件,用于APP端数据校验。
4.2 MQTT Topic规则与权限控制
阿里云为每个设备分配唯一的Topic权限,遵循严格路径规则。上行(设备→云)与下行(云→设备)Topic格式如下:
- 上行属性上报Topic:
/sys/{productKey}/{deviceName}/thing/event/property/post - 用途:STM32通过ESP8266向此Topic发布JSON数据,触发云端物模型解析。
权限:设备密钥自动授权,无需额外配置。
下行服务调用Topic:
/sys/{productKey}/{deviceName}/thing/service/property/set- 用途:APP通过云端下发指令(如“请求立即上报”),STM32需订阅此Topic并解析
params字段。 权限:需在“产品Topic类”中添加该Topic,并设置为“发布/订阅”。
自定义Topic(可选):
/user/{topicName}- 用途:用于APP与设备间的私有消息通信,如固件升级通知。
- 权限:必须在“产品Topic类”中显式声明,并勾选“发布”或“订阅”。
在“产品Topic类”管理页面,务必确认已添加上述Topic,并检查其权限状态为“已授权”。一个常见错误是仅配置了上行Topic,却忘记为下行Topic授权,导致APP下发的指令石沉大海。
4.3 规则引擎数据流转配置
物模型定义完成后,数据尚停留在云端,需通过规则引擎将其路由至APP。进入“规则引擎”→“创建规则”,配置如下:
- 数据源:选择刚创建的产品与设备,数据类型为“属性上报”。
- SQL处理:编写标准SQL过滤与转换。最简配置为:
sql SELECT temperature, time AS ts FROM "/sys/a1B2c3D4e5/my_device/thing/event/property/post" - 数据目的:添加“云产品流转”动作,目标选择“云消息队列(MNS)”或“消息服务(MQTT)”。若APP使用阿里云移动推送,则选择“移动推送”;若APP直连IoT MQTT,则选择“转发到另一个Topic”,目标Topic为
/user/app_update。
此步骤实现了数据的“一发多投”:同一份温度数据,既可写入时序数据库供Web查看,也可实时推送给APP。规则引擎的SQL能力还支持复杂场景,如“当temperature > 40时,触发告警邮件”。
5. Android APP核心逻辑与数据渲染
手机APP是用户交互的最终界面,其开发基于阿里云官方IoT Android SDK(com.aliyun.alink.linksdk:iot-linkkit-android:latest.release)。与传统HTTP轮询不同,它采用长连接MQTT,实现了真正的实时性。
5.1 设备认证与连接初始化
APP启动时,首要任务是完成设备身份认证。这并非登录账号,而是以“设备”身份接入IoT平台:
// 初始化LinkKit(单例) LinkKit.getInstance().init(new LinkKitInitParams()); // 构建设备信息(必须与STM32上传的三元组完全一致) final DeviceInfo deviceInfo = new DeviceInfo(); deviceInfo.productKey = "a1B2c3D4e5"; deviceInfo.deviceName = "my_device"; deviceInfo.deviceSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; // 设置初始化参数 LinkKitInitParams params = new LinkKitInitParams(); params.deviceInfo = deviceInfo; params.productKey = deviceInfo.productKey; params.deviceName = deviceInfo.deviceName; params.deviceSecret = deviceInfo.deviceSecret; // 异步初始化连接 LinkKit.getInstance().init(params, new ILinkKitConnectListener() { @Override public void onError(String s, String s1) { Log.e("IoT", "连接失败: " + s + ", " + s1); } @Override public void onInitDone(InitResult initResult) { Log.i("IoT", "连接成功"); // 订阅属性上报Topic,接收温度更新 subscribePropertyTopic(); } });LinkKit内部已封装完整的MQTT连接、TLS握手、心跳保活与断线重连逻辑,开发者只需关注业务回调。
5.2 属性变更监听与UI更新
温度数据通过/sys/{pk}/{dn}/thing/event/property/postTopic推送至APP。需注册监听器捕获此事件:
private void subscribePropertyTopic() { // 构建Topic(注意:必须与云端物模型Topic路径一致) String topic = "/sys/" + deviceInfo.productKey + "/" + deviceInfo.deviceName + "/thing/event/property/post"; // 订阅Topic MqttSubscribeRequest request = new MqttSubscribeRequest(); request.topic = topic; request.isQos0 = false; // 使用QoS1确保不丢消息 LinkKit.getInstance().subscribe(request, new ISubscribeListener() { @Override public void onSuccess() { Log.i("IoT", "订阅成功: " + topic); } @Override public void onFailure(Throwable throwable) { Log.e("IoT", "订阅失败", throwable); } }); } // 全局消息监听器(在Application类中注册) LinkKit.getInstance().registerOnMessageListener(new IOnMessageListener() { @Override public boolean onMessage(final String topic, final byte[] payload) { // 解析JSON数据 try { JSONObject json = new JSONObject(new String(payload)); JSONObject params = json.getJSONObject("params"); final double temp = params.getDouble("temperature"); // 切换到主线程更新UI(TextView) runOnUiThread(new Runnable() { @Override public void run() { temperatureTextView.setText(String.format("%.1f℃", temp)); // 可添加温度变化动画 temperatureTextView.startAnimation(rotateAnim); } }); } catch (JSONException e) { Log.e("IoT", "JSON解析失败", e); } return true; } });IOnMessageListener是全局回调,所有订阅Topic的消息均由此接收。runOnUiThread确保UI更新在主线程执行,避免CalledFromWrongThreadException。
5.3 实际开发中的坑与对策
在真实项目中,曾遇到三个高频问题:
连接频繁断开:源于APP后台运行时,Android系统(尤其EMUI、MIUI)会限制后台网络。对策是申请“电池优化白名单”,并在
AndroidManifest.xml中声明<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>,引导用户手动授权。温度显示延迟:并非网络问题,而是
TextView的setText()触发了完整的View重绘流程。对策是使用SpannableStringBuilder仅更新数字部分,或改用AppCompatTextView并开启硬件加速。JSON解析崩溃:云端偶尔推送格式异常的JSON(如缺少
params字段)。对策是在onMessage中添加完备的try-catch,并对json.has("params")、params.has("temperature")进行双重校验,失败时记录日志并忽略该条消息。
6. 端到端联调与故障排查方法论
系统集成后,90%的问题源于各环节间的“黑盒”状态。有效的联调必须建立清晰的观测点,将端到端链路拆解为可验证的原子单元。
6.1 分层诊断工具链
STM32层:使用ST-Link Utility或OpenOCD连接,实时监控
json_buffer内容。在build_temperature_json()末尾添加printf("JSON: %s\r\n", json_buffer);,并通过串口助手查看,确认JSON格式无语法错误(逗号、引号、括号配对)。ESP8266层:将ESP8266的TX引脚直连PC串口,使用
AT+UART_CUR?确认当前波特率,然后发送AT+CIPSTATUS查看TCP连接状态。若显示STATUS:4(TCP_CONNECTED),说明已连上阿里云;若为STATUS:2(GETTING_IP),则卡在DNS解析,需检查AT+CIPDOMAIN是否返回正确IP。云端层:IoT控制台的“监控运维”→“日志服务”是黄金工具。开启“设备上下线日志”与“消息收发日志”,可精确看到设备何时上线、哪条JSON被接收、是否触发规则引擎、是否有权限错误(如
403 Forbidden)。日志时间戳与设备本地HAL_GetTick()对比,可定位延迟来源。APP层:Android Studio的Logcat过滤
tag:IOT,观察onInitDone与onMessage的调用频率。若onInitDone成功但onMessage无回调,说明Topic订阅失败或云端未推送;若两者均有日志但UI无更新,问题必在UI线程切换或JSON解析逻辑。
6.2 典型故障场景与根因分析
场景一:APP显示“连接中”,但温度永不更新
-现象:STM32串口打印JSON正常,ESP8266AT+CIPSTATUS显示已连接,云端日志无设备消息。
-根因:ESP8266的MQTT Client ID配置错误。阿里云要求Client ID为{deviceName}|{productKey}|...,若STM32拼接时遗漏|或大小写错误,连接会被拒绝,但AT指令仍返回CONNECTED(这是AT固件的bug级设计)。
-验证:在AT+MQTTCONN后立即发送AT+MQTTCLEAN=0,再发送AT+MQTTSTAT,查看client_id字段是否与预期一致。
场景二:APP温度跳变,数值忽高忽低
-现象:Web控制台数据显示平稳,但APP每10秒刷新一次,且数值与Web不一致。
-根因:APP订阅了错误的Topic。例如,误订阅了/user/temperature而非/sys/{pk}/{dn}/thing/event/property/post,导致接收的是历史缓存消息或测试数据。
-验证:在IoT控制台“在线调试”中,手动向设备发送一条测试JSON,观察APP是否立即响应。若无响应,则Topic错误;若响应但数据旧,则是消息QoS等级或保留消息(Retained Message)设置问题。
场景三:设备频繁掉线,日志显示“DISCONNECTED”
-现象:每隔2-3分钟断开,重连后短暂恢复。
-根因:ESP8266未发送MQTT心跳(PINGREQ)。阿里云要求心跳间隔≤300秒,而AT固件默认为120秒,但若STM32发送数据间隔超过此值,连接将被服务器强制关闭。
-对策:在STM32主循环中,每90秒调用一次AT+MQTTPUB=0,"/sys/.../thing/event/property/post","{\"id\":\"1\",\"version\":\"1.0\",\"params\":{}}",0,0,发送空JSON维持心跳。此操作无业务意义,纯为保活。
6.3 性能与功耗优化实践
在电池供电的智能垃圾桶等场景中,功耗是硬约束。实测表明,ESP8266在Wi-Fi连接状态下电流达70mA,远超STM32的10mA。因此,最优策略是“按需唤醒”:
- STM32以10秒间隔采集温度,但仅在温度变化≥0.5℃时,才唤醒ESP8266(通过GPIO拉高EN引脚),发送数据后立即令其进入深度睡眠(
AT+GSLP=10000)。 - APP端关闭自动刷新,改为“下拉刷新”手动触发,避免后台持续心跳。
- 阿里云规则引擎中,将温度数据写入时序数据库的频率设为“每小时聚合一次”,减少云端写入压力。
这套组合策略可将整机待机电流从80mA降至5mA,电池寿命从3天延长至3个月。技术的本质不是堆砌功能,而是精准匹配场景需求,在性能、成本与功耗间找到最优解。
我在实际项目中部署这套方案时,曾因忽略ADC_CR2_TSVREFE位导致连续三天数据为0,最终通过示波器抓取ADC参考电压才定位问题。后来养成了一个习惯:每次新增外设,第一件事就是查阅Reference Manual中关于“Enable Bits”的章节,而不是直接抄例程。真正的嵌入式工程师,永远在手册与示波器之间往返穿行。