基于Sipeed M1s Dock制作的网络摄像头
2023寒假一起练 Sipeed M1s Dock wifi socket opencv 摄像头 视频
标签
嵌入式系统
Funpack活动
MPU
测试
aramy
更新2023-03-28
536

硬件介绍: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 接口。是个很新的板子FrPYG7Uq0r8ZwM-YURtDk8HYRJ5f

任务选择:这次选择完成项目5 -  网络相机。目标:完成一个基于Sipeed M1s Dock 的网络相机。具体要求:完成相机驱动,定时拍摄图片,并将图片通过网络传到电脑或服务器,实现长时间拍摄,通过电脑端编程将图片合成为一个视频。上位机部分使用python+Qt来实现,图片转换视频用opencv已有的功能来实现。

任务实现:Fpq8MmzTV2ipbEv1efPAjNriyfRf

Sipeed M1s Dock是一个很新的板子,网上的资料不多,所以一切都从官方提供的说明文档开始。按说明文档先搭建好编译环境,下载好官方例程。然后遇到了第一个坎。按官网的介绍,通过数据线接入OTG口,电脑会弹出一个7M大小的U盘。但是自己的板子始终没有弹出。在群里咨询,才知道固件版本的问题。第一步就变成了升级固件信息。
下载烧录工具Bouffalo Lab Eflash Command Tool
使用c型usb电缆从PC端连接到主板的UART端口
保持BOOT按钮被按下并点击RST按钮,然后释放BOOT按钮
启动BLDevCube Tools选择BL808芯片
根据下面的标签配置工具,最后单击Create & Download将开始下载固件

FjA_3O2HbVFVy9i8XEXbz6634tx1

升级好固件,查看官方提供的源码例程。通过项目名称可以看出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服务是没有问题的,所以问题只能是出在前端。
FlAZ4Z7QSVpO-UIHycLhGZZq32FY在linux下,使用greep的方式在源代码中搜索"Socket connect”字符串,发现“./M1s_BL808_SDK/components/sipeed/e907/m1s_e907_xram/src/m1s_e907_xram_wifi.c:        printf("Socket connect..\r\n");” 在这里能够找到这个字符串,意味着程序就是卡在了这里。
FvF721OPfkX_XnnqHqNJRSW43ke0
这里遇到了第二个坎。不明白为啥官方的例程中,是将服务端的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。
FqkmWaxxPwB-5np1wihtlpLsEPte
再次使用这个工具,将新编译好的估计写入开发板中。这时,开发板就能愉快地和服务器建立起连接啦!

梳理官方例程代码中的逻辑,下位机建立好连接后,就给服务器发送一个图片的长度,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()

FtyYoacldgxbJa5dDZc3EvAwBUuj

总结:非常感谢硬禾学堂举办的这次活动。荔枝派一直有关注他家的产品。这次的Sipeed M1s Dock带来了蛮多惊喜。自己水平有限,未能实现机器学习部分的任务,借助这次的学习机后续将继续向众多才华横溢的工程师朋友学习,玩转这块板子的神经网络功能。

 

附件下载
上位机.zip
下位机.zip
video1677121804.avi
团队介绍
瞎折腾小能手
团队成员
aramy
单片机业余爱好者,瞎捣鼓小能手。
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号