这次我们来看一个基于STM32的智能窗帘系统,它集成了WiFi、手机APP和云平台,实现了从本地到远程的完整物联网控制。这个项目的核心价值在于,它不是一个纯软件模拟,而是包含了硬件电路设计、单片机程序、移动端APP和云端数据交互的全栈实现,非常适合嵌入式物联网方向的开发者、电子爱好者以及希望将传统家居设备智能化的学习者进行复现和研究。
这个智能窗帘系统最值得关注的几个特点是:硬件成本可控,核心是STM32单片机搭配WiFi模块;控制方式多样,支持本地按键、手机APP远程、语音指令以及云端定时任务;数据可追溯,通过云平台可以查看窗帘的开合状态和历史记录。对于想入门物联网实战的开发者来说,这是一个非常典型的“端-管-云”架构案例。
本文将带你从零开始,拆解这个智能窗帘V5版本的设计与实现。我们会重点分析其硬件选型与电路连接、STM32程序的核心逻辑、手机APP的交互设计,以及如何将设备数据上报到云平台并实现反向控制。无论你是想复现整个项目,还是借鉴其中的WiFi配网、MQTT通信、多控制源优先级处理等关键技术点,都能找到清晰的思路和可操作的步骤。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 核心控制器 | STM32系列单片机(如STM32F103C8T6) |
| 网络连接 | ESP8266/ESP01S WiFi模块,支持STA/AP模式 |
| 控制方式 | 本地按键控制、手机APP远程控制、语音识别控制、云平台定时/场景控制 |
| 执行机构 | 直流电机或步进电机,通过电机驱动模块(如L298N、TB6612)控制 |
| 状态反馈 | 限位开关(物理终点检测)、霍尔传感器或编码器(位置反馈) |
| 移动端 | Android APP(可能基于Java或易安卓开发),支持状态显示与控制 |
| 云平台 | 常用物联网云平台(如OneNET、阿里云IoT、腾讯云IoT) |
| 通信协议 | MQTT/HTTP,用于设备与云平台、APP与云平台之间的数据交互 |
| 开发环境 | Keil MDK-ARM (STM32), Android Studio/易安卓 (APP), 云平台Web配置 |
| 适合场景 | 智能家居课程设计、毕业设计、物联网原型开发、传统窗帘智能化改造 |
2. 适用场景与使用边界
这个智能窗帘系统主要适用于以下几类人群和场景:
- 嵌入式与物联网学习者:作为一个完整的实战项目,学习STM32外设驱动、WiFi通信、MQTT协议、多任务调度等核心技能。
- 电子竞赛或毕业设计:项目功能完整,涉及硬件、软件、云端,具备良好的展示性和技术深度。
- 智能家居DIY爱好者:可以将此方案用于改造自家窗帘,实现自动化与远程控制。
- 产品原型开发:为智能窗帘类产品提供一个快速验证功能和可行性的原型框架。
在使用时,需要明确以下边界:
- 非商用成品:该项目通常作为学习原型,在稳定性、安全性、批量生产方面需要进一步优化才能投入商用。
- 依赖网络:远程控制功能依赖于稳定的本地WiFi和云平台服务,网络中断将导致远程控制失效。
- 电机选型与安全:实际应用中需根据窗帘重量和轨道选择合适的电机及驱动,并务必加入过流保护、堵转检测等安全机制,防止火灾风险。
- 语音控制局限性:通常采用离线语音识别模块(如LD3320),识别词条有限,复杂环境下降噪能力一般,适合固定指令控制。
- 隐私与数据安全:数据上传至第三方云平台,需关注平台的数据隐私政策。若对数据敏感,可考虑部署私有化MQTT服务器。
3. 环境准备与前置条件
在开始动手之前,请确保你已准备好以下软硬件环境。
硬件清单:
- 主控芯片:STM32F103C8T6最小系统板(核心板)。
- WiFi模块:ESP-01S(ESP8266),用于连接网络。
- 电机与驱动:
- 根据窗帘重量选择:小型窗帘可用5V-12V直流电机,重型窗帘需用步进电机或交流电机。
- 驱动模块:直流电机常用L298N或TB6612;步进电机常用A4988或DRV8825。
- 电源模块:为STM32、电机驱动、电机分别提供合适的电源(如5V、12V),注意功率足够。
- 输入设备:按键、红外遥控接收头、语音识别模块(如LD3320)。
- 传感器:两个限位开关(用于检测窗帘完全打开和完全关闭位置)。
- 其他:杜邦线、面包板或PCB、窗帘轨道及安装件。
软件与账号准备:
- 开发环境:
- STM32:Keil uVision 5 (MDK-ARM), 安装对应的STM32F1 Device Family Pack。
- 程序烧录:ST-Link V2 或 USB转TTL串口工具(用于ISP下载)。
- 云平台账号:注册一个物联网云平台账号,如中国移动OneNET(多协议接入)、阿里云物联网平台或腾讯云物联网开发平台。本文将以OneNET为例。
- 网络工具(可选但推荐):串口调试助手(如XCOM、SSCOM)、网络调试助手、MQTT客户端工具(如MQTT.fx)。
4. 硬件电路设计与连接
智能窗帘的硬件系统是基础,其稳定性和可靠性直接决定整个项目的成败。以下是核心模块的连接示意图和要点。
系统框图:
[手机APP] <---> [云平台] <---> [WiFi模块] <---> [STM32主控] <---> [电机驱动] <---> [电机] ^ ^ ^ ^ ^ ^ | | | | | | 远程控制 数据中转 网络接入 逻辑处理 功率放大 执行动作 | [按键/语音/限位开关] | 本地交互与状态反馈关键电路连接说明:
STM32与WiFi模块(ESP-01S):
- 通常通过串口(UART)通信,例如使用USART2。
ESP-01S TX->STM32 PA3 (USART2_RX)ESP-01S RX->STM32 PA2 (USART2_TX)ESP-01S VCC->3.3V(切记不可接5V!)ESP-01S GND->GNDESP-01S CH_PD/EN->3.3VESP-01S GPIO0悬空或接高电平(运行模式)。
STM32与电机驱动(以L298N驱动直流电机为例):
- 使用两个GPIO输出PWM信号控制速度和方向。
STM32 PA6 (TIM3_CH1)->L298N IN1(控制电机A方向)STM32 PA7 (TIM3_CH2)->L298N IN2(控制电机A方向)L298N OUT1, OUT2-> 直流电机两端。L298N的12V电源和GND接入外部电源,5V输出可给STM32供电(如果电源匹配)。
STM32与限位开关:
- 使用两个GPIO输入,配置为上拉输入模式。
限位开关1(开极限)->STM32 PB0, 常开触点,触发时接地。限位开关2(关极限)->STM32 PB1, 常开触点,触发时接地。- 开关另一端接GND。STM32内部上拉,未触发时为高电平,触发时为低电平。
STM32与按键/语音模块:
- 按键连接至GPIO输入,同样配置上拉。
- 离线语音识别模块(如LD3320)通常通过串口或SPI与STM32通信,发送固定的识别结果指令码。
电源设计注意事项:
- 隔离与稳压:电机(特别是启停时)会产生较大的电流波动和反向电动势,可能干扰微控制器。务必为STM32和数字电路部分使用独立的LDO(如AMS1117-3.3)供电,并与电机驱动电源隔离。
- 滤波电容:在STM32的电源入口、电机驱动芯片的电源引脚附近,并联足够容量的电解电容(如100uF)和瓷片电容(0.1uF)进行滤波。
5. STM32程序设计:核心逻辑与多控制源处理
STM32的程序是整个系统的“大脑”,需要处理传感器输入、执行控制输出,并协调本地与远程指令。其软件架构通常基于裸机前后台或RTOS。
5.1 程序主框架与初始化
// main.c 主函数框架示例 #include "stm32f10x.h" #include "usart.h" #include "motor.h" #include "limit_switch.h" #include "key.h" #include "esp8266.h" #include "mqtt.h" #include "timer.h" // 全局状态变量 typedef enum { CURTAIN_STOP, CURTAIN_OPENING, CURTAIN_CLOSING, CURTAIN_OPEN, CURTAIN_CLOSE } CurtainState_t; CurtainState_t g_curtain_state = CURTAIN_STOP; uint8_t g_target_percentage = 0; // 目标开合百分比 uint8_t g_current_percentage = 0; // 当前开合百分比(需编码器或时间估算) int main(void) { // 1. 系统时钟、外设初始化 SystemInit(); USART2_Init(115200); // 与ESP8266通信 USART1_Init(115200); // 调试输出 Motor_Init(); LimitSwitch_Init(); Key_Init(); TIM3_PWM_Init(7199, 0); // PWM频率=72M/(7199+1)=10kHz // 2. WiFi模块初始化与网络连接 ESP8266_Init(); while(ESP8266_ConnectAP("Your_WiFi_SSID", "Your_WiFi_Password") != SUCCESS) { Delay_ms(1000); } while(MQTT_Connect("设备ID", "产品APIKey") != SUCCESS) { // OneNET三元组 Delay_ms(1000); } MQTT_Subscribe("控制主题"); // 订阅云平台下发的控制指令 // 3. 主循环 while(1) { // 3.1 扫描本地输入(按键、限位开关) Key_Scan(); LimitSwitch_Scan(); // 3.2 处理网络数据(MQTT消息) MQTT_DataProcess(); // 3.3 执行状态机,控制电机 Curtain_StateMachine(); // 3.4 定时上报状态到云平台(例如每10秒) static uint32_t last_report = 0; if(HAL_GetTick() - last_report > 10000) { Report_Status_To_Cloud(); last_report = HAL_GetTick(); } Delay_ms(10); // 简单延时,或使用RTOS任务调度 } }5.2 多控制源优先级与冲突处理
这是项目的关键逻辑。当本地按键、APP指令、语音命令、云端定时任务几乎同时触发时,系统必须有明确的处理规则。
// control_manager.c 控制指令管理器 typedef enum { CMD_SOURCE_NONE = 0, CMD_SOURCE_LOCAL_KEY, // 本地按键,最高优先级 CMD_SOURCE_VOICE, // 语音 CMD_SOURCE_APP, // 手机APP CMD_SOURCE_CLOUD, // 云平台 CMD_SOURCE_AUTO_TIMER // 自动定时,最低优先级 } CmdSource_t; typedef struct { uint8_t cmd; // 命令:OPEN, CLOSE, STOP, SET_PERCENT uint8_t value; // 参数,如百分比 CmdSource_t source; uint32_t timestamp; } ControlCommand_t; // 指令队列(简易数组实现) ControlCommand_t cmd_queue[5]; uint8_t cmd_index = 0; // 指令处理函数 void Process_Control_Command(uint8_t cmd, uint8_t val, CmdSource_t src) { // 1. 优先级判断:如果正在执行高优先级指令,忽略低优先级新指令 // 例如:本地按键可打断APP的远程指令 if(src < g_current_cmd_source && g_curtain_state != CURTAIN_STOP) { printf("Higher priority command is running, ignore this one.\r\n"); return; } // 2. 限位保护:如果收到打开指令但已在开极限,则忽略 if(cmd == CMD_OPEN && LimitSwitch_OpenIsTriggered()) { printf("Already fully open.\r\n"); return; } // 关闭指令同理... // 3. 将有效指令存入队列或立即执行 cmd_queue[cmd_index].cmd = cmd; cmd_queue[cmd_index].value = val; cmd_queue[cmd_index].source = src; cmd_queue[cmd_index].timestamp = HAL_GetTick(); cmd_index = (cmd_index + 1) % 5; // 4. 更新当前执行的指令源 g_current_cmd_source = src; } // 在状态机中执行队列里的指令 void Curtain_StateMachine(void) { ControlCommand_t *cmd = &cmd_queue[0]; // 简化为处理队首 if(cmd->cmd == CMD_NONE) return; switch(g_curtain_state) { case CURTAIN_STOP: if(cmd->cmd == CMD_OPEN) { Motor_Run(OPEN_DIR, 70); // 70%速度打开 g_curtain_state = CURTAIN_OPENING; } else if(cmd->cmd == CMD_CLOSE) { Motor_Run(CLOSE_DIR, 70); g_curtain_state = CURTAIN_CLOSING; } else if(cmd->cmd == CMD_SET_PERCENT) { // 计算需要向哪个方向运动 if(cmd->value > g_current_percentage) { Motor_Run(OPEN_DIR, 70); g_curtain_state = CURTAIN_OPENING; } else if(cmd->value < g_current_percentage) { Motor_Run(CLOSE_DIR, 70); g_curtain_state = CURTAIN_CLOSING; } g_target_percentage = cmd->value; } cmd->cmd = CMD_NONE; // 指令已开始执行,清除 break; case CURTAIN_OPENING: if(LimitSwitch_OpenIsTriggered()) { Motor_Stop(); g_curtain_state = CURTAIN_OPEN; g_current_percentage = 100; Report_Status_To_Cloud(); // 触发状态上报 } // 检查是否有STOP指令 if(cmd->cmd == CMD_STOP) { Motor_Stop(); g_curtain_state = CURTAIN_STOP; cmd->cmd = CMD_NONE; } break; // ... CLOSING 状态类似 } }5.3 WiFi与MQTT通信实现
STM32通过AT指令与ESP8266模块交互,实现联网和MQTT通信。
// esp8266.c 关键函数示例 uint8_t ESP8266_SendCmd(char *cmd, char *expected_ack, uint32_t timeout) { USART2_SendString(cmd); // 通过串口发送AT指令 Delay_ms(100); // ... 等待并解析串口接收缓冲区,查找预期应答"OK"或"ERROR" return result; } uint8_t MQTT_Publish(char *topic, char *msg) { char send_buf[128]; // 构造OneNET的MQTT发布报文 (简化版) // 实际需按平台协议封装,例如:topic: $sys/{pid}/{device-name}/dp/post/json sprintf(send_buf, "AT+MQTTPUB=\"%s\",\"%s\",0,0\r\n", topic, msg); if(ESP8266_SendCmd(send_buf, "OK", 3000) == SUCCESS) { return SUCCESS; } return FAILURE; } // 在中断或轮询中处理ESP8266返回的数据 void USART2_IRQHandler(void) { if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) { char ch = USART_ReceiveData(USART2); // 将数据存入环形缓冲区 ring_buffer_write(&uart2_rx_buf, ch); // 如果收到换行符,通知解析任务 if(ch == '\n') { g_mqtt_msg_ready = 1; } } } // 解析MQTT消息 void MQTT_DataProcess(void) { if(!g_mqtt_msg_ready) return; char line[256]; ring_buffer_read_line(&uart2_rx_buf, line, sizeof(line)); // 示例:解析云平台下发的控制指令 // 假设平台下发JSON: {"cmd": "open", "percentage": 50} if(strstr(line, "+MQTTSUBRECV:")) { // 提取消息内容 char *msg_start = strchr(line, ','); msg_start = strchr(msg_start+1, ','); msg_start += 1; // 指向消息体开始 // 简单的JSON解析(实际项目建议用cJSON库) if(strstr(msg_start, "\"cmd\":\"open\"")) { Process_Control_Command(CMD_OPEN, 100, CMD_SOURCE_CLOUD); } else if(strstr(msg_start, "\"cmd\":\"set_percent\"")) { // 解析percentage值... int percent = 50; // 解析得到的值 Process_Control_Command(CMD_SET_PERCENT, percent, CMD_SOURCE_CLOUD); } } g_mqtt_msg_ready = 0; }6. 手机APP开发要点
手机APP作为重要的远程控制终端,其核心功能是:设备状态显示、发送控制指令、接收云端推送。开发可以选择原生Android(Java/Kotlin)、跨平台框架(Flutter/React Native)或易安卓等快速开发工具。
6.1 功能界面设计
一个典型的控制界面包含以下元素:
- 状态显示区:显示窗帘当前开合百分比(进度条)、文字状态(打开/关闭/停止)、网络连接状态。
- 控制按钮区:
- “打开”按钮:发送100%打开指令。
- “关闭”按钮:发送0%关闭指令。
- “停止”按钮:发送急停指令。
- 滑块(SeekBar):用于精确设置开合百分比(如50%)。
- 设置区:设备管理(添加/删除)、定时任务设置、情景模式(如“起床模式”7点打开)。
6.2 与云平台通信(以OneNET为例)
APP不直接与设备通信,而是通过云平台中转。这是标准的物联网架构。
// Android端示例代码 (简化,使用OkHttp) public class CloudAPI { private static final String API_KEY = "你的产品APIKey"; private static final String BASE_URL = "https://api.heclouds.com"; // 发送命令到设备 public static void sendCommand(String deviceId, String command) { OkHttpClient client = new OkHttpClient(); // 构建JSON数据点 String json = "{\"cmd\":\"" + command + "\"}"; RequestBody body = RequestBody.create(json, MediaType.parse("application/json")); Request request = new Request.Builder() .url(BASE_URL + "/cmds?device_id=" + deviceId) .post(body) .addHeader("api-key", API_KEY) .build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { Log.e("CloudAPI", "Send command failed", e); } @Override public void onResponse(Call call, Response response) throws IOException { if(response.isSuccessful()) { Log.i("CloudAPI", "Command sent successfully"); // 更新UI } } }); } // 查询设备最新状态 public static void queryDeviceStatus(String deviceId) { // 调用OneNET数据点查询API // https://api.heclouds.com/devices/{device_id}/datapoints // 解析返回的JSON,更新UI中的进度条和状态文字 } }APP与设备通信流程:
- APP用户点击“打开”按钮。
- APP构造JSON指令
{"cmd": "open"}。 - APP通过HTTPS POST请求,将指令发送至云平台API。
- 云平台通过已建立的MQTT长连接,将指令推送给在线的设备(ESP8266)。
- ESP8266通过串口将指令传给STM32。
- STM32解析并执行指令,控制电机转动。
- 设备将新的状态(如“已打开”)上报至云平台。
- 云平台将状态更新推送给APP(或APP定时轮询),APP刷新界面。
7. 云平台配置与数据流
云平台是连接设备与APP的桥梁。以中国移动OneNET平台为例,配置步骤如下:
- 创建产品:登录OneNET,在控制台创建新产品,选择“MQTT旧版”或“多协议接入”协议。
- 添加设备:在产品下添加一个设备,记录生成的
设备ID、产品ID和API Key(或设备鉴权信息)。这三元组需要写入STM32程序中。 - 定义数据流:创建数据流(Data Stream),例如
curtain_status用于上报状态,control_cmd用于下发指令。 - 创建设备触发器(可选):可以设置规则,例如当
curtain_status变为“已关闭”时,自动向APP推送一条通知。 - 查看数据:在设备的数据展示页面,可以实时看到设备上报的数据点图表。
设备端上报数据的JSON格式示例:
{ "id": "123", "dp": { "curtain_status": [ { "v": 65, // 当前开合百分比 65% "t": 1678888888 } ], "motor_current": [ { "v": 0.45, // 电机电流值,可用于故障检测 "t": 1678888888 } ] } }8. 系统联调与功能验证
将所有部分连接好后,按以下步骤进行系统测试:
硬件上电与基础测试:
- 给系统上电,观察各模块指示灯(STM32、ESP8266)。
- 使用USB转TTL工具连接STM32的调试串口(USART1),打开串口助手,查看启动日志,确认程序正常运行,WiFi模块AT指令交互正常。
网络连接测试:
- 在串口日志中,应看到ESP8266连接指定WiFi成功的提示。
- 随后应看到MQTT连接云平台成功的提示。
- 登录云平台控制台,查看设备状态应为“在线”。
本地控制测试:
- 按下本地“打开”按键,观察电机是否正向转动,直到触发“开”限位开关停止。
- 按下“关闭”按键,电机应反向转动,直到触发“关”限位开关停止。
- 在转动过程中按下“停止”键,电机应立即停止。
- 观察云平台控制台,设备是否同步上报了状态变化。
远程控制测试:
- 打开手机APP,确保APP已绑定正确的设备。
- 在APP上点击“打开”,观察窗帘是否动作,同时APP界面状态应更新。
- 测试百分比控制,拖动滑块到50%,观察窗帘是否运动到中间位置附近停止(需要编码器或时间估算实现位置反馈,否则此功能不精确)。
语音控制测试:
- 对语音模块说出预设的指令词,如“打开窗帘”,观察是否响应并执行正确动作。
云端定时测试:
- 在云平台或APP上设置一个定时任务,例如“每天8:00打开窗帘”。
- 到达设定时间,观察设备是否自动执行。
9. 常见问题与排查方法
在开发调试过程中,你可能会遇到以下典型问题:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| ESP8266无法连接WiFi | 1. SSID/密码错误 2. WiFi信号弱 3. 模块供电不足 | 1. 检查代码中SSID/密码 2. 用AT指令手动测试 ( AT+CWJAP?)3. 测量ESP8266 VCC电压(需稳定3.3V) | 1. 修正密码 2. 靠近路由器 3. 使用独立3.3V LDO供电,确保电流>500mA |
| MQTT连接云平台失败 | 1. 产品ID/设备ID/API Key错误 2. 网络问题 3. 平台协议选择错误 | 1. 核对三元组 2. 用网络调试助手测试TCP连接 3. 检查OneNET产品是否为“MQTT旧版” | 1. 重新复制粘贴三元组 2. 检查路由器防火墙 3. 在平台端重新创建MQTT设备 |
| 设备在线但APP控制无反应 | 1. APP与设备未绑定同一产品 2. 数据流名称不匹配 3. 指令格式错误 | 1. 检查APP内的设备ID 2. 对比设备上报和APP订阅的数据流名 3. 抓包查看MQTT消息内容 | 1. 在APP重新绑定设备 2. 统一数据流命名(如 control)3. 按照平台文档构造JSON |
| 电机不转或抖动 | 1. 电源功率不足 2. 驱动模块使能端未接 3. PWM频率或占空比不对 4. 程序逻辑未执行到驱动部分 | 1. 测量电机两端电压,带载时是否跌落严重 2. 检查L298N的ENA/ENB是否使能 3. 用示波器或逻辑分析仪看PWM波形 4. 调试程序,检查控制函数是否被调用 | 1. 更换功率更大的电源 2. 将ENA短接到5V或接STM32 PWM 3. 调整PWM初始化参数 4. 添加调试输出,逐步定位 |
| 限位开关失灵 | 1. 开关机械故障 2. GPIO配置错误(应为上拉输入) 3. 触发时电平变化未检测到 | 1. 用万用表测量开关通断 2. 检查GPIO初始化代码 3. 在中断或扫描函数中打印电平值 | 1. 更换限位开关 2. 确认配置 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU3. 添加软件去抖动 |
| 位置控制不准 | 1. 未使用编码器等位置传感器 2. 使用时间估算,但电机速度不稳 3. 皮带/轨道打滑 | 1. 确认反馈方式 2. 测量电机从全关到全开的时间,校准时间系数 3. 检查机械结构 | 1. 增加旋转编码器 2. 多次测量取平均时间,并考虑启动停止的加减速 3. 紧固机械连接 |
10. 项目优化与扩展方向
完成基础功能后,可以考虑以下方向进行优化和扩展,让项目更完善、更实用:
- 增加精确位置反馈:为电机加装旋转编码器,实时计算窗帘的精确位置,实现高精度的百分比控制。
- 引入光线传感器:根据环境光照强度自动控制窗帘开合,实现真正的“智能”。
- 加入电流检测:通过采样电阻和ADC检测电机电流,实现堵转、过流保护,提升安全性。
- 实现本地场景联动:与其他传感器(如温湿度、人体红外)联动,本地逻辑判断,减少对云端的依赖。
- 开发iOS版本APP:使用跨平台框架(如Flutter)或原生开发,覆盖更多用户。
- 搭建私有云:使用EMQX等开源MQTT Broker,搭配Node-RED或自写服务端,将数据完全掌握在自己手中。
- 优化功耗:在无人时段或深夜,让STM32和ESP8266进入低功耗休眠模式,由定时器或外部中断唤醒。
- 设计美观外壳:使用3D打印或亚克力板为控制系统制作一个美观且安全的外壳。
这个基于STM32的智能窗帘项目,从硬件电路到云端应用,完整地走通了物联网的典型链路。它不仅仅是一个控制电机的程序,更是一个涉及嵌入式开发、无线通信、移动开发、云服务集成等多个技术领域的综合实践。通过复现和深入研究这个项目,你能够系统地掌握物联网设备从概念到实物的全过程,为开发更复杂的智能硬件产品打下坚实的基础。建议在动手时,先从最简单的本地按键控制电机开始,每完成一个模块就充分测试,最后再集成联网和远程功能,这样能更清晰地定位问题,稳步推进。