ESP32 MPY 多任务异步框架,需要支持多个ModbusTCP连接,使用POLL同步IO多路复用实现这个目的,引出了"混合方案",这种模式在嵌入式系统中很常见,既能利用传统socket编程,又能获得异步并发的优势。
1、请问下面ESP32 MPY的POLL IO多路复用测试代码"events = poller.poll(1000) # 1秒超时",这是异步协程吗?是否在1000ms内会主动让出CPU使用权?
import socket, select, machine
server_socket = socket.socket()
try:
server_socket.bind(('0.0.0.0', 8080)) # 0000绑定任意可用IP
except Exception as e:
machine.reset()
server_socket.listen(3) # 允许3个挂起连接
print(server_socket.fileno()) # 54
poller = select.poll() # select.select没有select.poll高级
poller.register(server_socket, select.POLLIN)
clients = {} # {fd: (conn, address)} 文件描述用二元组socket.accept()
while True:
events = poller.poll(1000) # 1秒超时
#print(events) # [] --> [(<socket>, 1)]有事件发生就处理
for s, event in events:
#print(fd) # <socket>
if s == server_socket: # 新连接 .fileno()
conn, addr = server_socket.accept()
print("新客户端:", addr) # ('192.168.2.104', 53064)
conn.setblocking(False)
poller.register(conn, select.POLLIN)
clients[conn] = (conn, addr)
conn.send(b"Connected!\r\n")
else: # 客户端数据
conn, addr = clients[s]
data = conn.recv(512)
if data:
print(f"{addr} 发送:", data)
conn.send(data) # Echo 回传
else: # 断开
print(f"{addr} 断开")
poller.unregister(conn)
conn.close()
del clients[conn]
这段代码是 同步阻塞 的 I/O 多路复用,不是异步协程。在MicroPython中,select.poll()是一个同步的I/O多路复用机制,它会阻塞直到有事件发生或者超时。具体来说,代码中的poller.poll(1000)会阻塞最多1000毫秒(1秒),直到有注册的文件描述符(在这里是socket)有事件发生(比如可读)。在阻塞期间,它不会主动让出CPU使用权。也就是说,在这1秒内,如果没有任何事件发生,程序会一直等待,直到超时后才继续执行后面的代码。如果有事件发生,它会立即返回事件列表,然后处理这些事件。因此,这段代码是同步的,而不是异步的。在同步代码中,如果没有事件发生,程序会阻塞在poll()调用上,不会执行其他任务。在异步协程中,我们通常使用asyncio库,其中会有事件循环,并且使用await来让出CPU,以便在等待I/O时执行其他协程。
虽然这段代码是同步的,但它通过select.poll 实现了单线程处理多个连接,可以在一个线程中同时监听多个socket,避免了为每个连接创建线程的开销。仍然是同步编程模型,只是通过轮询实现了伪并发。关于CPU使用率:在 poll(1000) 阻塞期间,线程是休眠状态,CPU 可以执行其他任务,**但在MicroPython的单线程环境中,如果没有其他线程,CPU实际上在等待 I/O 事件。**这比传统的 accept() 阻塞要高效,因为可以同时等待多个连接
2、使用poll.poll(1)同步阻塞1ms,将这段poll内容改写为asyncio异步函数调用可否?因为我还有其他异步任务(modbus库、传感器数据采集)需要执行。
poll.poll(1) + await asyncio.sleep_ms(0) 确实可以实现伪并发。
# 方案1:混合模式 - poll在异步协程中
'''
poll_task函数中每处理完一次轮询就让出CPU(通过await asyncio.sleep(0))。这样,其他协程(如other_tasks)就有机会运行。但是,这种方式仍然有轮询开销,且不是事件驱动的。真正的异步IO是事件驱动的,当没有事件时,CPU是休眠的。而上面的混合方案中,即使没有事件,也会每1毫秒轮询一次,然后让出CPU,这会导致事件循环频繁唤醒,增加功耗和CPU占用。因此,我仍然推荐使用asyncio的异步TCP服务器,它是事件驱动的,在没有事件时不会占用CPU。请根据你的实际需求选择方案。如果你的其他异步任务也是基于asyncio的,那么使用asyncio的TCP服务器是最佳选择。如果你的其他任务无法改为异步,那么你可能需要考虑使用多线程或上面的混合方案。
'''
import socket, select, machine
import uasyncio as asyncio
class PollBasedServer:
def __init__(self, port=8080):
self.server_socket = socket.socket()
self.server_socket.bind(('0.0.0.0', port))
self.server_socket.listen(3)
self.server_socket.setblocking(False) # 非阻塞
self.poller = select.poll()
self.poller.register(self.server_socket, select.POLLIN)
self.clients = {}
async def run(self):
"""异步运行poll服务器"""
print("Poll服务器启动...")
while True:
try:
# 只等待10ms,不会长时间阻塞
events = self.poller.poll(10)
for s, event in events:
if s == self.server_socket:
await self.handle_new_connection() # 虽然是同步的,但保持await风格
else:
await self.handle_client_data(s) # 虽然是同步的,但保持await风格
# 让出CPU给其他异步任务
# 协作式多任务的核心机制
# 在同步代码混合异步框架时必不可少
# 真正的协程切换点
await asyncio.sleep_ms(0)
except Exception as e:
print("服务器错误:", e)
'''
函数内部并没有使用异步的IO操作,它们使用的是非阻塞socket的同步读写。所以,这两个函数实际上不会挂起,而是同步执行。
这样,如果有一个客户端发送大量数据,那么处理这些数据可能会占用较长时间,从而阻塞其他协程。
为了解决这个问题,考虑在数据处理时也使用异步方式,或者确保数据处理时间很短,然后通过频繁的await asyncio.sleep_ms(0)来让出控制权。
'''
async def handle_new_connection(self):
"""处理新连接"""
conn, addr = self.server_socket.accept()
print("新客户端:", addr)
conn.setblocking(False)
self.poller.register(conn, select.POLLIN)
self.clients[conn] = (conn, addr)
conn.send(b"Connected!\r\n")
async def handle_client_data(self, sock):
"""处理客户端数据"""
conn, addr = self.clients[sock]
try:
data = conn.recv(512)
if data:
print(f"{addr} 发送:", data)
conn.send(data) # Echo 确保数据处理时间很短
else: # 断开
print(f"{addr} 断开")
self.poller.unregister(conn)
conn.close()
del self.clients[conn]
except:
# 连接可能已断开
if sock in self.clients:
del self.clients[sock]
# 其他异步任务示例
async def modbus_task():
"""模拟Modbus任务"""
while True:
print("Modbus任务执行中...")
# 这里调用你的Modbus库
await asyncio.sleep(5) # 每5秒执行一次
async def sensor_task():
"""模拟传感器采集任务"""
while True:
print("采集传感器数据...")
# 这里调用传感器采集代码
await asyncio.sleep(5) # 每5秒执行一次
async def main():
# 创建poll服务器实例
server = PollBasedServer(8080)
# 同时运行多个异步任务
await asyncio.gather(
server.run(), # poll服务器
modbus_task(), # Modbus任务
sensor_task(), # 传感器任务
)
# 启动事件循环
asyncio.run(main())
测试结果:
MicroPython v1.27.0 on 2025-12-09; Generic ESP32 module with ESP32
Type "help()" for more information.
>>> %Run -c $EDITOR_CONTENT
MPY: soft reboot
ESP32 IP NETMASK GATEWAY DNS
('192.168.1.119', '255.255.255.0', '192.168.1.1', '8.8.8.8')
Signal strength: -47
('192.168.1.119', '255.255.255.0', '192.168.1.1', '8.8.8.8')
WebREPL server started on http://192.168.1.119:8266/
Started webrepl in normal mode
Poll服务器启动...
Modbus任务执行中...
采集传感器数据...
新客户端: ('192.168.1.110', 63382)
('192.168.1.110', 63382) 发送: b'3333333333333333'
Modbus任务执行中...
采集传感器数据...
Modbus任务执行中...
采集传感器数据...
新客户端: ('192.168.1.110', 63398)
('192.168.1.110', 63398) 发送: b'111111111111'
('192.168.1.110', 63382) 发送: b'3333333333333333'
新客户端: ('192.168.1.110', 63405)
Modbus任务执行中...
采集传感器数据...
('192.168.1.110', 63398) 发送: b'111111111111'
('192.168.1.110', 63405) 发送: b'2222222222222'
('192.168.1.110', 63382) 发送: b'3333333333333333'
('192.168.1.110', 63398) 发送: b'111111111111'
('192.168.1.110', 63405) 发送: b'2222222222222'
('192.168.1.110', 63382) 发送: b'3333333333333333'
('192.168.1.110', 63398) 发送: b'111111111111'
('192.168.1.110', 63405) 发送: b'2222222222222'
('192.168.1.110', 63382) 发送: b'3333333333333333'
Traceback (most recent call last):
File "<stdin>", line 107, in <module>
File "asyncio/core.py", line 1, in run
File "asyncio/core.py", line 1, in run_until_complete
File "<stdin>", line 31, in run
KeyboardInterrupt:
>>>
微观执行流程:
假设有3个任务:A(poll服务器)、B(Modbus)、C(传感器)
时间线:
-
任务A执行 → poll.poll(1)等待1ms
-
任务A执行 → 处理事件(如果有)
-
任务A执行 → 遇到 await asyncio.sleep_ms(0)
-
↑ 事件循环切换:保存任务A状态,切换到任务B
-
任务B执行 → 执行一部分Modbus操作
-
任务B遇到 await 或完成一段 → 切换回事件循环
-
事件循环切换:可能切换到任务C
-
任务C执行 → 采集传感器数据
-
任务C完成一段 → 切换回事件循环
-
事件循环发现任务A就绪 → 切换回任务A
-
任务A从 await asyncio.sleep_ms(0) 后面继续执行
其他方案2:更简洁的异步TCP服务器。如果你的需求不复杂,可以直接用asyncio的TCP服务器。
异步框架+POLL混合方案非常合适:

保持高性能:POLL的快速响应
支持并发:异步框架的多任务能力
资源高效:适合ESP32限制
代码可控:同步处理逻辑清晰
易于调试:问题定位简单
这种架构在工业控制、物联网网关等场景中非常实用,既能处理高速网络通信,又能并行执行Modbus、传感器采集等任务。
锦上添花:
-
动态调整POLL频率
async def poll_server_task():
server = PollTCPServer(8080)poll_interval = 1 # 默认1ms while True: server.poll_events() # 根据负载动态调整 client_count = len(server.clients) if client_count == 0: await asyncio.sleep_ms(10) # 无客户端时降低频率 elif client_count > 5: await asyncio.sleep_ms(0) # 高负载时立即让出 else: await asyncio.sleep_ms(poll_interval)
2、批量处理优化
def poll_events(self):
"""批量处理优化版本"""
events = self.poller.poll(0)
# 按事件类型分组处理
new_connections = []
data_events = []
error_events = []
for fd, event in events:
if fd == self.server:
new_connections.append(fd)
elif event & select.POLLIN:
data_events.append(fd)
elif event & select.POLLERR:
error_events.append(fd)
# 批量处理(更高效)
for fd in new_connections:
self._accept_connection()
for fd in data_events:
self._receive_data(fd)
for fd in error_events:
self._close_connection(fd)
3、添加看门狗保护