一.项目需求
- 完成相机驱动,定时拍摄图片,并将图片通过网络传到电脑或服务器,实现长时间拍摄。
- 通过电脑端编程将图片合成为一个视频。
二.环境配置
1.M1s 需要在 Linux 环境下进行编译,使用git获取例程仓库与SDK仓库。
例程仓库:
git clone https://gitee.com/Sipeed/M1s_BL808_example.git
SDK仓库:
git clone https://gitee.com/sipeed/M1s_BL808_SDK.git
2.获取编译工具链并配置编译工具链路径。
在SDK文件夹下创建编译链文件夹,之后使用git获取编译链。将编译工具重命名为Linux_x86_64。
mkdir -p M1s_BL808_SDK/toolchain
cd M1s_BL808_SDK/toolchain
git clone https://gitee.com/wonderfullook/m1s_toolchain.git
mv m1s_toolchain Linux_x86_64
获取SDK当前路径。
cd M1s_BL808_SDK
pwd
将SDK路径导入系统环境,添加语句export BL_SDK_PATH=pwd所得路径。
cd ~
vi ./bashrc
export BL_SDK_PATH=/home/wushu/workspace/m1s_dock/M1s_bl808_SDK
编译环境结构树如图所示。
三.程序实现
官方例程里已给出关于wifi流传摄像头画面的例程,为满足个人需求对部分程序进行修改与功能增加。
1.E907核心固件修改
1.1图像流传
因官方SDK在e907wifi流传程序ip与端口固定死了,存在一些问题(官方已修复),固进行更改。
文件路径为:M1s_BL808_SDK/components/sipeed/e907/m1s_e907_xram/src/m1s_e907_xram_wifi.c
其中存在 static void upload_stream_task(void *param) 函数,需修改部分如下。
client_addr.sin_family = AF_INET;
client_addr.sin_port = htons(8888);
client_addr.sin_addr.s_addr = inet_addr("10.42.0.1");
memset(&(client_addr.sin_zero), 0, sizeof(client_addr.sin_zero));
IP与端口信息已存储至private结构体中,固将程序修改如下。
client_addr.sin_family = AF_INET;
client_addr.sin_port = htons(private.port);
client_addr.sin_addr.s_addr = inet_addr(private.ip);
memset(&(client_addr.sin_zero), 0, sizeof(client_addr.sin_zero));
重新编译e907 固件,将编译结构存在于M1s_BL808_example/e907_app/build_out中,将其烧录至板卡即刻。
cd ./M1s_BL808_example/e907_app
./build.sh firmware
1.2摄像头画面尺寸
为实现在LCD屏幕中预览摄像头所拍摄画面,对摄像头初始化函数进行修改。被注释部分为未修改前函数。
int bl_cam_mipi_rgb565_init(void)
{
int ret = 0;
CAM_CFG_Type camcfg =
{
.swMode = CAM_SW_MODE_MANUAL,
.swIntCnt = 0,
.pixWidth = CAM_PIX_DATA_BIT_24TO16,
.dropMode = CAM_DROP_NONE,
.linePol = CAM_LINE_ACTIVE_POLARITY_HIGH,
.framePol = CAM_FRAME_ACTIVE_POLARITY_HIGH,
.camSensorMode = CAM_SENSOR_MODE_V_AND_H,
.burstType = CAM_BURST_TYPE_INCR64,
.waitCount = 0x40,
.memStart = NULL, //need init
.memSize = 0,
// .memSize = SCALER_WIDTH*8*2*2,
// .frameSize = RGB565_FRAME_SIZE,
.frameSize = SCALER_WIDTH*SCALER_HEIGHT*2,
};
if (rgb565_pic_buf == NULL) {
// rgb565_pic_buf = pvPortMalloc(RGB565_FRAME_SIZE * FRAME_COUNT);
rgb565_pic_buf = pvPortMalloc(SCALER_WIDTH*SCALER_HEIGHT*2 * FRAME_COUNT);
if (NULL == rgb565_pic_buf) {
printf("malloc rgb565 pic buf fail\r\n");
ret = -1;
goto exit;
}
}
DSP2_MISC_Scaler_Cfg_Type scaler_cfg =
{
.inputWidth = MIPI_WIDTH,
.inputHeight = MIPI_HEIGHT,
// .outputWidth = RGB565_SCALER_WIDTH,
// .outputHeight = RGB565_SCALER_HEIGHT,
.outputWidth = SCALER_WIDTH,
.outputHeight = SCALER_HEIGHT,
};
bl_cam_mipi_csi_init();
DSP2_MISC_Scaler_Input_Select(DSP2_MISC_SCALER_2_ID, DSP2_MISC_SCALER_DSP2_INPUT);
DSP2_MISC_Scaler_Init(DSP2_MISC_SCALER_2_ID, &scaler_cfg);
DSP2_MISC_Scaler_Enable(DSP2_MISC_SCALER_2_ID);
DSP2_Scaler_Set_Input(DSP2_MISC_SCALER_C, DSP2_MISC_SCALER_DSP2_INPUT);
DSP2_YUV2RGB_Init(DSP2_YUV2RGB_PARAM_8BIT_BT601);
DSP2_YUV2RGB_Set_Input(DSP2_YUV2RGB_A, DSP2_YUV2RGB_INPUT_SCALER_C);
DSP2_MISC_CAM_Input_Select(DSP2_MISC_CAM_5_ID, DSP2_MISC_CAM_YUV2RGB_OUTPUT);
camcfg.memStart = (uint32_t)(uintptr_t)rgb565_pic_buf;
camcfg.memSize = SCALER_WIDTH*SCALER_HEIGHT*2 * FRAME_COUNT;
// camcfg.memSize = RGB565_FRAME_SIZE * FRAME_COUNT;
CAM_Init(RGB565_CAM_USE_ID, &camcfg);
CAM_16_Bit_RGB_order(RGB565_CAM_USE_ID, CAM_16_BIT_BGR);
CAM_Enable(RGB565_CAM_USE_ID);
exit:
return ret;
}
此方式实则不妥,初始化摄像头两次,分别使用了
bl_cam_mipi_mjpeg_init();
bl_cam_mipi_rgb565_init();
初始化mjpeg是为了流传画面,初始化rgb565是为了在lcd中显示。利用使用摄像头后pop的间隔时间,切换使用。mjpeg获取摄像头画面尺寸是rgb565画面尺寸的四倍,故进行修改。
实际上,最初想在上位机pc端进行画面预览,但在实际使用上预览画面是一张又一张图片显示后关掉,刷新率较低,故放弃使用。
后想将获取的mjpeg进行解码为rgb,再放到lcd中显示。然后发现获取mjpeg并流传都是e907内核中一线程完成。想要在c906中获取mjpeg数据也受挫,尝试使用一中间变量传递数据,但发现最后解码出来的都有错误。后想多核间通信,查阅发现使用AXI4总线进行多核间通信交换数据,但并未找到详细使用方法。
最后无奈,因能力不行,使用此笨办法。
2.c906程序部分
2.1夜间LED
最初试想在夜间或黑暗场所点亮板载led进行补光与照亮,初始化LED与按键的GPIO,按下按键后改变LED电平。
/* Flash_led */
#define PIN_LED (8)
#define LED_ON false
#define LED_OFF true
#define PIN_BTN1 (22)
bool led_flag=LED_OFF;
static void Flash_led_init()
{
GLB_GPIO_Cfg_Type cfg;
cfg.drive = 0;
cfg.smtCtrl = 1;
cfg.gpioFun = GPIO_FUN_GPIO;
cfg.outputMode = 0;
cfg.pullType = GPIO_PULL_NONE;
cfg.gpioPin = PIN_LED;
cfg.gpioMode = GPIO_MODE_OUTPUT;
GLB_GPIO_Init(&cfg);
cfg.gpioPin = PIN_BTN1;
cfg.gpioMode = GPIO_MODE_INPUT;
GLB_GPIO_Init(&cfg);
GLB_GPIO_Write(PIN_LED, led_flag);
}
static void Flash_led()
{
if(!GLB_GPIO_Read(PIN_BTN1))
{
led_flag ^= LED_OFF;
GLB_GPIO_Write(PIN_LED, led_flag);
vTaskDelay(500);
}
}
2.2LCD显示
static void gettolcd_task(void *param)
{
while(1)
{
// socket_sw();
while (0 != bl_cam_mipi_rgb565_frame_get((uint8_t **)&picture, &len))
{
vTaskDelay(1);
}
BilinearInterpolation_RGB565(picture, 800 ,600, draw_buf, 280, 240);
for (int i = 0; i < 280 * 240; i++)
{
draw_buf[i] = __builtin_bswap16(draw_buf[i]);
}
st7789v_spi_draw_picture_nonblocking(0, 0, 279, 239, draw_buf);
bl_cam_mipi_rgb565_frame_pop();
vTaskDelay(100);
}
}
2.3TCP流传部分
此为官方例程程序,在c906中只是起到传递wifi ssid、password与TCP客户端IP与端口的功能,实际工作在e907中。
m1s_xram_wifi_init();
m1s_xram_wifi_connect("404Not Found", "404404404");
m1s_xram_wifi_upload_stream("192.168.3.10", 8888);
3.上位机程序
上位机使用python语言,构建一个TCP服务端与M1s dock进行tcp连接,在连接成功后,上位机打印客户端ip地址与端口,之后便可接收mpeg数据。
初次M1s dock会发送接下来图片长度,将其设置为上位机下次接收数据长度并回复任意数据给M1s以应答,让其继续发送。之后每次将发送一帧jpeg数据流与下一帧数据流长度,我们将下一帧数据流长度分离出来并利用。循环往复。
同时,使用opencv将没帧数据流写入形成视频文件,存储至指定路径。
import socket
import cv2
import io
import numpy as np
if __name__ == '__main__':
fps =10 # 视频帧率
size = (800, 600) # 视频分辨率
# 视频参数
video = cv2.VideoWriter("G:/Workspace/output.avi", cv2.VideoWriter_fourcc('I', '4', '2', '0'), fps, size)
tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口复用,使程序退出后端口马上释放
tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
#心跳
tcp_server.ioctl(socket.SIO_KEEPALIVE_VALS, (1, 60 * 1000, 30 * 1000))
# 绑定端口
tcp_server.bind(("", 8888))
# 设置监听
tcp_server.listen(128)
#客户端的ip,端口号
tcp_client, tcp_client_address = tcp_server.accept()
print("客户端的ip地址和端口号:", tcp_client_address)
#超时设置
socket.setdefaulttimeout(2)
#初次获取长度帧
recv_data = tcp_client.recv(4)
frame_len = int.from_bytes(recv_data, "little")
send_data = "n".encode()
while True:
if recv_data:
tcp_client.send(send_data)
# 接收帧数据
recv_data = tcp_client.recv(frame_len+4)
if (recv_data[0] == 255 and recv_data[1] == 216) and len(recv_data)==(frame_len+4):
byte_stream = io.BytesIO(recv_data)
file_bytes = np.asarray(bytearray(byte_stream.read()), dtype=np.uint8)
img = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
video.write(img)
print("ok")
else :
print("err,repairing..")
m_len=0
while len(recv_data)<(frame_len+4):
m_len=frame_len+4-len(recv_data)
m_recv_data = tcp_client.recv(m_len)
recv_data += m_recv_data
a=recv_data.split(b'\xff\xd9')[1]
frame_len = int.from_bytes(a, "little")
else:
tcp_client.close()
exit(0)
四.总结
此任务官方给出了例程,在很大程度上减少了制作难度。摄像头驱动无需自己完成,TCP+socket也是写好的,直接使用就行。在写上位机与板子建立通信时弄了好久,后发现是sdk里程序写死了,有点坑。解决后主要的问题就是jpeg数据流的识别与处理还有将其合成为视频的问题,在众多资料的帮助下逐渐学习与理解,最后解决了相关问题,学习了知识。
本次项目算是勉强完成任务的要求,实现基本功能,但做的并不好。有很多想要实现的附加功能,但由于个人能力限制与资料较少未能完成。希望之后官方能放出更多资料,可以让我们进一步学习,做出更好的东西。