功能介绍
本项目使用 M1s Dock 开发板和上位机 Python GUI程序实现了一个网络相机,包含实时网络流图像传输显示、视频录制功能。M1s Dock 开发板是基于博流智能科技的 BL808 芯片设计的一款 AIOT 模组,主控芯片包含三个 RISC-V 核心,具有 WiFi/BT/BLE/Zigbee 等无线互联单元,包含多个 CPU 以及音频编码译码器、视频编码译码器和 AI 硬件加速器(BLAI-100),适用于各种高性能和低功耗应用领域。
本项目利用 M1s Dock 开发板上的 MIPI 摄像头采集图像,并通过 socket 协议将图像数据转发到其他网络设备。上位机使用 python GUI 程序接收图像数据,并进行实时显示。上位机也可以通过 GUI 界面控制视频录制功能,并通过 opencv 将实时图像数据写入到视频文件中。
硬件介绍
-
主芯片 BL808 RISC-V 480Mhz + NPU BLAI-100
-
板载 USB 转 UART 调试器(可实现一键点击烧录,无需按实体按键)
-
1.69 寸 240x280 电容触摸屏
-
200W 像素摄像头
-
支持 2.4G WIFI / BT / BLE
-
板载 1 个模拟麦克风、1 个 LED、1 个 TF 卡座
-
引出一路 USB-OTG 到 USB Type-C 接口
M1s dock 开发板有两个核心,分别是 BL808 芯片的 C906 核心和 E907 核心。
-
C906 核心是一个 RV64GCV 架构的 RISC-V 处理器,运行在 480MHz 的频率,支持 AI NN 通用硬件加速器 BLAI-100,用于视频/音频检测/识别等任务。
-
E907 核心是一个 RV32GCP 架构的 RISC-V 处理器,运行在 320MHz 的频率,支持 Wi-Fi 802.11 b/g/n 和 Bluetooth 5.x Dual-mode (BT+BLE) 等无线功能。
一般情况下对于 C906 进行编程。
整体架构
项目整体分成两个部分,嵌入式端和上位机:
-
嵌入式端:通过 WIFI 接入网络,使用 socket 协议将捕获的图片传输到指定的地址
-
上位机:建立 socket 服务,接收来自 MCU 的图片,并通过 GUI 进行显示,支持通过 opencv 将图片写入到视频文件
实现细节
在嵌入式端,Sipeed 官方已经提供了图像传输相关的函数。刚拿到开发板时,SDK 中的 socket 推流函数不能够正确传参,但官方在后续的更新中解决了这个问题,新版 SDK 可以正常工作。
在实现代码时,我遇到了一个困难,开发板出厂自带的 firmware 不能运行 camera_stream_through_wifi 例程,需要更新 firmware 才能正常工作。这是因为开发板上的 BL808 芯片有两个 RISC-V 核心,分别是 E907 和 C906,分别运行不同的 firmware。出厂自带的 firmware 可能不支持某些功能或者有 bug,所以需要更新到最新的 firmware 才能保证正常运行。
更新 firmware 的方法有两种,一种是通过 U 盘烧录,一种是通过串口烧录。U 盘烧录只能更新 C906 核心的 firmware,串口烧录可以同时更新 E907 和 C906 核心的 firmware。我选择了串口烧录的方法,具体步骤如下:
-
使用 TypeC 数据线将电脑与开发板的 UART 口连接起来,在电脑上会显示两个串口设备。
-
下载博流官方烧录工具 BLDevCube,并在软件中选择 BL808 芯片。
-
选择分区表文件 partition_cfg_16M_m1sdock.toml,以及 boot2、firmware 和 d0fw 三个固件文件。boot2 是固定的,位于 BLDevCube\chips\bl808\builtin_imgs\boot2_isp_bl808_xxxx_xxx 目录下;firmware 是 E907 核心运行的固件;d0fw 是 C906 核心运行的固件。
-
在窗口右侧点击 Refresh 来刷新串口,并选择串口号较大的那个串口。
-
按住开发板上的 BOOT 键和 RST 键,然后先松开 RST 键再松开 BOOT 键来使开发板进入串口烧录模式。
-
点击 Create & Download 后会开始烧录过程。如果握手失败或者超时,请重新操作第 5 步和第 6 步。
-
烧录完成后,开发板会自动重启,并运行最新的 firmware。
程序整体流程如图所示,可以分为 GUI 和图像接受线程两部分,线程被启动之后开启服务器循环接收来图片,并存储到成员变量当中,GUI 需要刷新时通过成员变量获取具体的图像数据。
# 封装 socket 接收图片类
# 类内会为接收图片创建一个线程,线程内会不断接收图片数据
# 接收到图片数据后,会将图片数据保存到类的成员变量中
# 类的成员变量可以通过 get_image_data() 方法获取
# 包含录制视频功能
class SocketImageReceiver:
def __init__(self, ip, port):
self._ip = ip
self._port = port
self._image_data = None
self._thread = None
self._is_running = False
self._is_recording = False
self._video_writer = None
def start(self):
self._thread = threading.Thread(target=self._run)
self._thread.setDaemon(True)
self._thread.start()
def stop(self):
# 如果正在录制视频,则停止录制视频
if self._is_recording:
self.stop_record()
self._is_running = False
self._thread.join()
def start_record(self):
# 创建视频写入对象
# 参数1:文件名
# 参数2:编码器,这里使用 DIVX 编码器
# 参数3:帧率
# 参数4:图像大小
# 图像大小从类的成员变量中获取
# 文件以时间戳命名,避免文件名重复
fname = time.strftime("%Y%m%d%H%M%S", time.localtime()) + ".avi"
self._video_writer = cv.VideoWriter(fname, cv.VideoWriter_fourcc(*'DIVX'), 15, self._image_data.shape[1::-1])
self._is_recording = True
print("开始录制视频")
def stop_record(self):
self._is_recording = False
self._video_writer.release()
print("停止录制视频")
def _run(self):
# 创建tcp服务端套接字
tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口号复用,让程序退出端口号立即释放,否则的话在30秒-2分钟之内这个端口是不会被释放的,这是TCP的为了保证传输可靠性的机制。
tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 给客户端绑定端口号,客户端需要知道服务器的端口号才能进行建立连接。IP地址不用设置,默认就为本机的IP地址。
tcp_server.bind((self._ip, self._port))
# 设置监听,把主动套接字变为被动套接字,被动套接字只负责接收客户端的连接请求,accept方法会等待客户端的连接请求,如果有客户端连接过来,就会返回一个新的套接字专门为这个客户端服务。
tcp_server.listen(128)
# 等待客户端的连接请求
print("等待客户端的连接请求...")
self._is_running = True
tcp_client, client_addr = tcp_server.accept()
print("客户端已连接,客户端的ip地址为:", client_addr)
while self._is_running:
# 接收客户端发送的数据, 这次接收数据的最大字节数是4
recv_data = tcp_client.recv(4)
mjpeg_len = int.from_bytes(recv_data, 'little')
# print("recv len: ", mjpeg_len)
tcp_client.send(recv_data)
recv_data_mjpeg = b''
remained_bytes = mjpeg_len
while remained_bytes > 0:
recv_data_mjpeg += tcp_client.recv(remained_bytes)
remained_bytes = mjpeg_len - len(recv_data_mjpeg)
# print("recv stream success")
if recv_data_mjpeg[:2] != b'\xff\xd8' \
or recv_data_mjpeg[-2:] != b'\xff\xd9':
continue
mjpeg_data = np.frombuffer(recv_data_mjpeg, 'uint8')
img = cv.imdecode(mjpeg_data, cv.IMREAD_COLOR)
self._image_data = img
# 如果当前正在录制 则将图像数据写入视频文件
if self._is_recording:
self._video_writer.write(img)
tcp_client.close()
tcp_server.close()
def get_image_data(self):
return self._image_data
总结
本项目展示了 M1s 开发板的强大功能和灵活性,可以实现多种网络相机的应用场景,如视频监控、智能门铃、远程会议等。同时也体现了 RISC-V 架构的优势,可以支持多种操作系统和开发方式。