硬件介绍:Sipeed M1s Dock 是基于 Sipeed M1s 模组来设计的一款核心板,主芯片 BL808 RISC-V 480Mhz + NPU BLAI-100。板载 USB 转 UART 调试器(可实现一键点击烧录,无需按实体按键)1.69 寸 240x280 电容触摸屏;200W 像素摄像头
支持 2.4G WIFI / BT / BLE;板载 1 个模拟麦克风、1 个 LED、1 个 TF 卡座引出了 MIPI CSI、SPI LCD 等 FPC 接口。两个type-C接口对应USB-OTG和USB Type-C 接口。是个很新的板子
任务选择:这次选择完成项目5 - 网络相机。目标:完成一个基于Sipeed M1s Dock 的网络相机。具体要求:完成相机驱动,定时拍摄图片,并将图片通过网络传到电脑或服务器,实现长时间拍摄,通过电脑端编程将图片合成为一个视频。上位机部分使用python+Qt来实现,图片转换视频用opencv已有的功能来实现。
任务实现:
Sipeed M1s Dock是一个很新的板子,网上的资料不多,所以一切都从官方提供的说明文档开始。按说明文档先搭建好编译环境,下载好官方例程。然后遇到了第一个坎。按官网的介绍,通过数据线接入OTG口,电脑会弹出一个7M大小的U盘。但是自己的板子始终没有弹出。在群里咨询,才知道固件版本的问题。第一步就变成了升级固件信息。
下载烧录工具Bouffalo Lab Eflash Command Tool;
使用c型usb电缆从PC端连接到主板的UART端口
保持BOOT按钮被按下并点击RST按钮,然后释放BOOT按钮
启动BLDevCube Tools选择BL808芯片
根据下面的标签配置工具,最后单击Create & Download将开始下载固件
升级好固件,查看官方提供的源码例程。通过项目名称可以看出camera_streaming_through_wifi这个项目就是将摄像头内容通过wifi传给上位机的,正好是选择任务的功能。打开这个项目查看源代码,发现源码非常简单,就简单的几行,将内部的 wifi和IP 换成自己的局域网。
#include <stdbool.h>
#include <stdio.h>
/* FreeRTOS */
#include <FreeRTOS.h>
#include <task.h>
/* bl808 c906 std driver */
#include <bl808_glb.h>
#include <bl_cam.h>
#include <m1s_c906_xram_wifi.h>
void main()
{
vTaskDelay(1);
bl_cam_mipi_mjpeg_init();
m1s_xram_wifi_init();
m1s_xram_wifi_connect("ZEUS", "zeus123456");
m1s_xram_wifi_upload_stream("192.168.1.14", 8888);
}
然后在局域网内PC机上启动了socker服务,监听着8888端口。结果不出意外地不行。电脑上socket服务程序收不到任何连接。在开发板上通过串口工具查看日志,发现前端卡在"Socket connect”,一直循环。检查电脑端的socket服务是没有问题的,所以问题只能是出在前端。在linux下,使用greep的方式在源代码中搜索"Socket connect”字符串,发现“./M1s_BL808_SDK/components/sipeed/e907/m1s_e907_xram/src/m1s_e907_xram_wifi.c: printf("Socket connect..\r\n");” 在这里能够找到这个字符串,意味着程序就是卡在了这里。
这里遇到了第二个坎。不明白为啥官方的例程中,是将服务端的IP写死在程序中的,这样的后果就是,无论调用程序中如何设置服务端的IP地址,这里都不会去连接。经过讨论群里的老师指导,将这段代码修做修改。
static void upload_stream_task(void *param)
{
while (0 == private.got_ip) {
vTaskDelay(1);
}
int ret = 0;
uint32_t mjpeg_start_addr, mjpeg_buffer_size;
uint8_t *pic, *usb_ptr;
uint32_t len, first_len, second_len;
int sock = -1;
struct sockaddr_in client_addr;
_retry:
ret = bl_cam_mjpeg_buffer_info_get(&mjpeg_start_addr, &mjpeg_buffer_size);
if (ret != 0) {
printf("mjpeg not init\r\n");
vTaskDelay(50);
goto _retry;
}
printf("mjpeg init is ok!\r\n");
#define PER_FRAME_MJPEG 120*1024
if (private.stream_buff) {
vPortFree(private.stream_buff);
private.stream_buff = NULL;
}
private.stream_buff = pvPortMalloc(PER_FRAME_MJPEG);
if (NULL == private.stream_buff) {
printf("malloc fail!\r\n");
goto exit;
}
while (1) {
vTaskDelay(100);
printf("Socket connect..\r\n");
if (0 > (sock = socket(AF_INET, SOCK_STREAM, 0))) {
continue;
}
client_addr.sin_family = AF_INET;
client_addr.sin_port = htons(private.port);
client_addr.sin_addr.s_addr = inet_addr(private.ip);
// client_addr.sin_port = htons(8888);
// client_addr.sin_addr.s_addr = inet_addr("192.168.1.14");
memset(&(client_addr.sin_zero), 0, sizeof(client_addr.sin_zero));
if(-1 == connect(sock,
(struct sockaddr *)&client_addr,
sizeof(struct sockaddr)))
{
closesocket(sock);
continue;
}
while (1) {
ret = bl_cam_mjpeg_get(&pic, &len);
csi_dcache_invalid_range((void *)pic, len);
if (ret == 0) {
if (((uint32_t)(uintptr_t)pic + len) > (mjpeg_start_addr + mjpeg_buffer_size)) {
/* if mjpeg store edge loop to start*/
first_len = mjpeg_start_addr + mjpeg_buffer_size - (uint32_t)(uintptr_t)pic;
second_len = len - first_len;
csi_dcache_invalid_range((void *)pic, first_len);
memcpy(private.stream_buff, pic, first_len);
csi_dcache_invalid_range((void *)mjpeg_start_addr, second_len);
memcpy(private.stream_buff + first_len, (void *)mjpeg_start_addr, second_len);
usb_ptr = private.stream_buff;
} else {
/*mjpeg data not cut*/
usb_ptr = pic;
csi_dcache_invalid_range((void *)usb_ptr, len);
}
uint8_t recv;
_retry2:
printf("send jpg len(%d):%ld\r\n", sizeof(len), len);
if (write(sock, &len, sizeof(len)) < 0) break;
if (read(sock, &recv, 1) < 0) {
vTaskDelay(50);
goto _retry2;
}
#define PACK_LEN (1000)
int remain_len = len;
if (write(sock, usb_ptr, remain_len) < 0) break;
bl_cam_mjpeg_pop();
}
}
closesocket(sock);
}
exit:
vTaskDelete(NULL);
}
然后在./M1s_BL808_example/e907_app 目录下,执行./build.sh firmware 就会将修改好的代码编译成新的固件,新的固件保存在 ./M1s_BL808_example/e907_app/build_out/firmware.bin。
再次使用这个工具,将新编译好的估计写入开发板中。这时,开发板就能愉快地和服务器建立起连接啦!
梳理官方例程代码中的逻辑,下位机建立好连接后,就给服务器发送一个图片的长度,4个字节长度的整形数据;然后上位机返回任意信息给下位机,下位机接到返回信息后,将图片内容发送给上位机。如此循环往复。
按此逻辑,使用python搭建起socket服务。使用opencv展示图片,使用QT做了个简单的用户界面。当用户选择录像时,就使用opencv中的VideoWriter函数,将图片写入视频文件中。
HOST = '192.168.1.14' #主机号为空白表示可以使用任何可用的地址。
PORT = 8888 #端口号
class TcpServer(Thread):
"""Tcp服务器"""
def __init__(self):
"""初始化对象"""
super().__init__()
self.code_mode = "utf-8" # 收发数据编码/解码格式
self.server_socket = socket(AF_INET, SOCK_STREAM) # 创建socket
self.server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, True) # 设置端口复用
self.server_socket.bind((HOST, PORT)) # 绑定IP和Port
self.server_socket.listen(5) # 设置为被动socket
self.recodevideo=False #是否录像
def run(self):
"""运行"""
while True:
client_socket, client_addr = self.server_socket.accept() # 等待客户端连接
print("{} online".format(client_addr))
tr = Thread(target=self.recv_data, args=(client_socket, client_addr)) # 创建线程为客户端服务
tr.start() # 开启线程
self.server_socket.close()
def recv_data(self, client_socket, client_addr):
"""收发数据"""
height=0
width=0
layers=0
video=None
rec=False
while True:
try:
data = client_socket.recv(1024) #第一个收到的数据应该为4位的图片长度
imgleng=struct.unpack("i",data)[0]
print("Recv image size %d H:%d W:%d,Channl:%d" %(imgleng,height, width, layers))
client_socket.send(struct.pack("?", True)) #返回一个数据,提示客户端 继续发送
if imgleng> 1:
data = client_socket.recv(imgleng) #接收图片信息
# filename="img"+str(time.time())+".jpg"
# filename = "img.jpg"
# filename="%03d" %(num)+".jpg"
# with open(filename, "wb") as f:
# f.write(data)
img_buffer_numpy = np.frombuffer(data, dtype=np.uint8) # 将 图片字节码bytes 转换成一维的numpy数组 到缓存中
img = cv2.imdecode(img_buffer_numpy, cv2.COLOR_RGBA2BGR) # 从指定的内存缓存中读取一维numpy数据,并把数据转换(解码)成图像矩阵格式
height, width, layers = img.shape
cv2.imshow("M1s", img)
cv2.waitKey(1) # 等待按键
if self.recodevideo==True and rec==False: #开始录像
video = cv2.VideoWriter('video'+ str(time.time())[:10]+".avi",
cv2.VideoWriter_fourcc(*'DIVX'),
10, (width,height))
rec=True
video.write(img)
elif self.recodevideo==False and rec==True:
video.release()
rec = False
elif self.recodevideo==True and rec==True:
video.write(img)
except:
pass
cv2.destroyAllWindows()
client_socket.close()
总结:非常感谢硬禾学堂举办的这次活动。荔枝派一直有关注他家的产品。这次的Sipeed M1s Dock带来了蛮多惊喜。自己水平有限,未能实现机器学习部分的任务,借助这次的学习机后续将继续向众多才华横溢的工程师朋友学习,玩转这块板子的神经网络功能。