跳转至

摄像头应用

一、摄像头介绍

Air780EPM 搭载了一个摄像头模块,能够实时采集图像并将数据通过串口传输至电脑,方便用户进行图像处理和分析。同时,摄像头还支持 LCD 屏幕上的实时预览,提供直观的视觉反馈,帮助用户更好地进行图像捕捉和调试。

二、硬件环境

参考:硬件环境清单第二章节内容,准备以及组装好硬件环境。

1. 我们需要按照按照下图方式进行接线,插入 LCD 拍照,摄像头黄色部分连接电路板。

需要注意:LCD 的 BLK 引脚是悬空的。

三、软件环境

“凡事预则立,不预则废。”在详细阐述本功能示例之前,我们需先精心筹备好以下软件环境。

1. Luatools 工具

2. 内核固件文件(底层 core 固件文件):LuatOS-SoC_V2003_Air780EPM;参考项目使用的内核固件

3. luatos 需要的脚本和资源文件

SSCOM 串口工具

脚本和资源文件点我,查看 demo 链接

lib 脚本文件:使用 Luatools 烧录时,勾选 添加默认 lib 选项,使用默认 lib 脚本文件;

准备好软件环境之后,接下来查看如何烧录项目文件到 Air780EPM 核心板,将本篇文章中演示使用的项目文件烧录到 Air780EPM 核心板中。

四、摄像头基本用法

4.1 本教程实现功能定义

当前教程将要实现如下功能:

1、配合 lcd,在 lcd 屏幕上显示下摄像头拍摄到的照片

2、开发板给电脑 sscom 发送摄像头拍摄的图像数据,并在电脑端打开

3、利用摄像头解析二维码数据,并在 Luatools 显示二维码具体含义

4.2 文章内容应用

Air780EPM 开发板软硬件资料 :https://docs.openluat.com/air780epm/luatos/hardware/design/

API 参考 : https://docs.openluat.com/air780epm/luatos/api/

4.3 API 介绍

camera.on(id, event, func)

注册摄像头事件回调。

参数

**参数**
**类型**
**释义**
**取值**
id
int
camera id
camera 0写0, camera 1写1
event
string

事件名称
字符串
func
function
回调函数
回调函数

返回值

**返回值**
**类型**
**释义**
**取值**
nil
nil
无返回值
nil

例子

camera.on(0, "scanned", function(id, str)
    if type(str) == 'string' then -- 如果是扫码模式(使用摄像头对二维码、条形码或其他类型的图案进行扫描和识别)
        log.info("扫码结果", str)
    elseif str == false then -- 如果摄像头没有正常工作
        log.error("摄像头没有数据")
    else -- 如果摄像头正常工作,并且不是扫码模式
        log.info("摄像头数据", str)
        sys.publish("capture done", true)
    end
end) -- 注册 camera 0 的 "scanned" 事件

camera.preview(id, onoff)

启停 camera 预览功能,直接输出到 LCD 上,只有硬件支持的 SOC 可以运行。

参数

**参数**
**类型**
**释义**
**取值**
id
int
camera id
camera 0写0, camera 1写1
onoff
boolean
启停camera预览功能
true开启,false停止

返回值

**返回值**
**类型**
**释义**
**取值**
onoff
boolean
是否成功启动camera预览功能
成功返回true,否则返回false

例子

camera.preview(camera_id, true) -- 打开LCD预览功能(直接将摄像头数据输出到LCD)

camera.capture(id, save_path, quality)

启动摄像头拍照功能。

参数

**参数**
**类型**
**释义**
**取值**
id
int
camera id
camera 0写0, camera 1写1
save_path
string/zbuff/nil
摄像头拍照后的数据存放路径
string/nil:文件保存路径,空则写在上次路径里,默认是/capture.jpg
zbuff:将图片保存在buff内不写入文件系统
quality
int
jpeg压缩质量
1最差,占用空间小,3最高,占用空间最大而且费时间,默认1

返回值

**返回值**
**类型**
**释义**
**取值**
onoff
boolean
是否成功启动摄像头拍照功能。完成后通过camera.on设置的回调函数回调接收到的长度
成功返回true,否则返回false

例子

-- 将摄像头拍照的数据存入 "/testCamera.jpg" 文件中
-- jpeg压缩质量1最差,占用空间小,3最高,占用空间大,2和3需要非常多非常多的psram,尽量不要用
camera.capture(camera_id, "/testCamera.jpg", 1)

camera.close(id)

完全关闭指定的 camera,释放相应的 IO 资源。

参数

**参数**
**类型**
**释义**
**取值**
id
int
camera id
camera 0写0, camera 1写1

返回值

**返回值**
**类型**
**释义**
**取值**
onoff
boolean
是否成功关闭指定摄像头
成功返回true,否则返回false

例子

camera.close(0)

camera.stop(id)

暂停摄像头捕获数据。仅停止了图像捕获,未影响预览功能。

注意:调用该函数 camera.preview 的 LCD 预览功能依旧存在。

参数

**参数**
**类型**
**释义**
**取值**
id
int
camera id
camera 0写0, camera 1写1

返回值

**返回值**
**类型**
**释义**
**取值**
onoff
boolean
是否成功暂停指定摄像头
成功返回true,否则返回false

例子

camera.stop(0)

五、摄像头整体演示

5.1 源码

源码 :https://gitee.com/openLuat/LuatOS-Air780EPM/blob/master/demo/camera/spi_cam/main.lua

5.2 演示

5.2.1 代码实现

  • main.lua
PROJECT = "camera_30W_480_320_demo"
VERSION = "4.0.0"
-- 实际使用时选1个就行
-- require "bf30a2"
require "gc032a"
-- require "gc0310"
sys = require("sys")
log.style(1)

pm.ioVol(pm.IOVOL_ALL_GPIO, 3000)

--  mcu.altfun(mcu.I2C, 0, 66, 2, nil)
--  mcu.altfun(mcu.I2C, 0, 67, 2, nil)

gpio.setup(2,1)--GPIO2打开给camera_3.3V供电
gpio.setup(28,1)--GPIO28打开给lcd3.3V供电

gpio.setup(14, nil)
gpio.setup(15, nil)

local SCAN_MODE = 0 -- 写1演示扫码(使用摄像头对二维码、条形码或其他类型的图案进行扫描和识别)
local scan_pause = true -- 扫码启动与停止标志位
local getRawStart = false
local RAW_MODE = 0 -- 写1演示获取原始图像
-- SCAN_MODE和RAW_MODE都没有写1就是拍照

------------------------------------
------------ 初始化 LCD ------------
------------------------------------
-- 根据不同的BSP返回不同的值
-- spi_id,pin_reset,pin_dc,pin_cs,bl
local function lcd_pin()
    local rtos_bsp = rtos.bsp()
    if string.find(rtos_bsp,"780EPM") then
        return lcd.HWID_0, 36, 0xff, 0xff, 0xff -- 注意:EC718P有硬件lcd驱动接口, 无需使用spi,当然spi驱动也支持
    else
        log.info("main", "没找到合适的cat.1芯片",rtos_bsp)
        return
    end
end
local spi_id, pin_reset, pin_dc, pin_cs, bl = lcd_pin()
if spi_id ~= lcd.HWID_0 then
    spi_lcd = spi.deviceSetup(spi_id, pin_cs, 0, 0, 8, 20 * 1000 * 1000, spi.MSB, 1, 0)
    port = "device"
else
    port = spi_id
end

lcd.init("st7796", {
    port = port,
    pin_dc = pin_dc,
    pin_pwr = bl,
    pin_rst = pin_reset,
    direction = 0,
    -- direction0 = 0x00,
    w = 320,
    h = 480,
    xoffset = 0,
    yoffset = 0,
    sleepcmd = 0x10,
    wakecmd = 0x11,
})

------------------------------------
------------ 初始化串口 -------------
------------------------------------
local uartid = uart.VUART_0 -- 根据实际设备选取不同的uartid
-- 初始化
local result = uart.setup(uartid, -- 串口id
600000, -- 波特率
8, -- 数据位
1 -- 停止位
)

------------------------------------
----------- 初始化摄像头 -----------
------------------------------------
camera.on(0, "scanned", function(id, str)
    if type(str) == 'string' then -- 如果是扫码模式(使用摄像头对二维码、条形码或其他类型的图案进行扫描和识别)
        log.info("扫码结果", str)
    elseif str == false then -- 如果摄像头没有正常工作
        log.error("摄像头没有数据")
    else -- 如果摄像头正常工作,并且不是扫码模式
        log.info("摄像头数据", str)
        sys.publish("capture done", true)
    end
end) -- 注册 camera 0 的 "scanned" 事件

------------------------------------
-------- 注册 boot 按键中断 ---------
------------------------------------
local function press_key()
    log.info("boot press")
    sys.publish("PRESS", true)
end
gpio.setup(0, press_key, gpio.PULLDOWN, gpio.RISING)
gpio.debounce(0, 100, 1)
local rawbuff, err
if RAW_MODE ~= 1 then
    -- 如果 RAW_MODE 不等于 1,创建一个大小为 60KB 的缓冲区
    rawbuff, err = zbuff.create(60 * 1024, 0, zbuff.HEAP_AUTO)
else
    -- 如果 RAW_MODE 等于 1
    rawbuff, err = zbuff.create(640 * 480 * 2, 0, zbuff.HEAP_AUTO) -- 创建一个大小为 640x480 像素,每个像素占用 2 字节的缓冲区,适用于 gc032a 摄像头
    -- local rawbuff = zbuff.create(240 * 320 * 2, zbuff.HEAP_AUTO)  -- 创建一个大小为 240x320 像素,每个像素占用 2 字节的缓冲区,适用于 bf302a 摄像头
end
if rawbuff == nil then
    log.info(err)
end

--------------------------------------------------
---- 将文件系统中存储的jpg文件通过串口发送给电脑 ----
--------------------------------------------------
local function sendFile()
    sys.taskInit(
        function()
            local fileHandle = io.open("/testCamera.jpg","rb")
            -- log.info("文件大小",#fileHandle)
            if not fileHandle then
                log.error("打开文件失败")
                return            else
                log.info("文件打开成功,文件大小为",io.fileSize("/testCamera.jpg"))
            end

            while true do
                local data = fileHandle:read(1460)
                -- log.info("data我看看",data)
                if not data then break end
                log.info("虚拟uart发送数据",uart.write(uartid, data))
                sys.wait(10)
                -- sys.waitUntil("UART_SENT2MCU_OK")
            end
            fileHandle:close()
    end)
end

sys.taskInit(function()
    log.info("摄像头启动")

    -- 初始化摄像头
    local cspiId, i2cId = 1, 1 -- spi的id和摄像头的id
    local camera_id
    i2c.setup(i2cId, i2c.FAST) -- I2C1设置为快速模式,不开启上拉
    gpio.setup(5, 0) -- 将 GPIO5 下拉,用于 SPI 片选信号
    -- camera_id = bf30a2Init(cspiId,i2cId,25500000,SCAN_MODE,SCAN_MODE)
    -- camera_id = gc0310Init(cspiId, i2cId, 25500000, SCAN_MODE, SCAN_MODE)
    camera_id = gc032aInit(cspiId,i2cId,24000000,SCAN_MODE,SCAN_MODE) -- 通过 I2C1 配置摄像头,SPI1 时钟频率为 24 MHz
    camera.stop(camera_id) -- 暂停摄像头捕获数据。仅停止了图像捕获,未影响预览功能。
    camera.preview(camera_id, true) -- 打开LCD预览功能(直接将摄像头数据输出到LCD)

    log.info("按下boot开始测试")
    -- 总内存大小,单位字节;当前已使用的内存大小,单位字节;历史最高已使用的内存大小,单位字节
    log.info(rtos.meminfo("sys")) -- 打印系统内存信息
    log.info(rtos.meminfo("psram")) -- 打印PSRAM内存信息
    while 1 do
        result, data = sys.waitUntil("PRESS", 30 * 1000) -- 等待 "PRESS" 事件 30 秒(等待boot按键被按下)
        if result == true and data == true then -- 如果 30 秒内检测到 "PRESS" 事件往下执行
            if SCAN_MODE == 1 then
                if scan_pause then
                    log.info("启动扫码")
                    -- camera_id = gc0310Init(cspiId,i2cId,25500000,SCAN_MODE,SCAN_MODE)
                    camera.start(camera_id)
                    scan_pause = false
                    sys.wait(200)
                    log.info(rtos.meminfo("sys"))
                    log.info(rtos.meminfo("psram"))
                else
                    log.info("停止扫码")
                    -- camera.close(camera_id)  --完全关闭摄像头才用这个
                    camera.stop(camera_id)
                    scan_pause = true
                    sys.wait(200)
                    log.info(rtos.meminfo("sys"))
                    log.info(rtos.meminfo("psram"))
                end
            elseif RAW_MODE == 1 then -- 摄像头捕获原始数据
                if getRawStart == false then
                    getRawStart = true
                    log.debug("摄像头首次捕获原始图像")
                    camera.startRaw(camera_id, 640, 480, rawbuff) -- gc032a
                    -- camera.startRaw(camera_id,240,320,rawbuff) --bf302a
                else
                    log.debug("摄像头继续捕获原始图像")
                    camera.getRaw(camera_id)
                end
                result, data = sys.waitUntil("capture done", 30000)
                log.info("摄像头捕获原始图像完成")
                camera.stop(camera_id) -- 暂停摄像头捕获数据。仅停止了图像捕获,未影响预览功能。
                log.info(rtos.meminfo("sys"))
                log.info(rtos.meminfo("psram"))
                -- uart.tx(uartid, rawbuff) --找个能保存数据的串口工具保存成文件就能在电脑上看了, 格式为JPG
            else -- 拍照模式
                log.debug("摄像头拍照")
                -- camera_id = gc0310Init(cspiId,i2cId,25500000,SCAN_MODE,SCAN_MODE)
                -- 将摄像头拍照的数据存入 "/testCamera.jpg" 文件中
                -- jpeg压缩质量1最差,占用空间小,3最高,占用空间大,2和3需要非常多非常多的psram,尽量不要用
                camera.capture(camera_id, "/testCamera.jpg", 1) -- 2和3需要非常多非常多的psram,尽量不要用
                result, data = sys.waitUntil("capture done", 30000)
                -- log.info(rawbuff:used())
                -- camera.close(camera_id)  --完全关闭摄像头才用这个
                camera.stop(camera_id) -- 暂停摄像头捕获数据。仅停止了图像捕获,未影响预览功能。
                sendFile() -- 创建一个任务将摄像头数据通过串口发送到电脑
                -- rawbuff:resize(60 * 1024)
                -- log.info(rtos.meminfo("sys"))
                -- log.info(rtos.meminfo("psram"))
            end

        end
    end

end)

-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后后面不要加任何语句!!!!!

5.2.2 程序烧录

1. 参考二章节与三章节完成好硬件和软件环境的搭建。

2. 此时按照如下步骤进行烧录程序。

3. 按照如下步骤将 core 文件和 demo 例程烧录。

5.2.3 现象复现

LCD 显示摄像头数据

当开发板启动后,摄像头拍摄的图像将会在 LCD 屏幕上显示出来。

图片数据上传至电脑

当我们按下 boot 按键,摄像头拍摄的图像数据将通过 USB 模拟的串口上传至电脑。其操作步骤如下

1. 如下两个写 0,然后下载程序。当下载完成程序后,SSCOM 工具的端口号将会出现三个 USB 串行设备。

local SCAN_MODE = 0 -- 写1演示扫码(使用摄像头对二维码、条形码或其他类型的图案进行扫描和识别)
local RAW_MODE = 0 -- 写1演示获取原始图像

2. 当我们选中其中一个 USB 串行设备,并点击打开串口,可能会出现如下报错。

3. 如果出现报错,那么就更改 USB 串行设备端口,三个中有一个不会报错,我这边是 COM29 端口不会报错。

4. 点击接收数据到文件,之后会出现一个弹框,表示接收到的数据存入的路径,我们打开文件管理器中的该目录。

5. 此时我们按下 boot 按键。

6. 在 Luatools_中将会看到如下打印信息。

7. 此时在电脑的对应路径下将会出现一个 DAT 文件。

8. 我们将其文件后缀名修改为 jpg 即可。需要注意:每次按下 boot 按键拍摄照片,在电脑端都需要将原来的 DAT 格式文件删除。否则你修改为 jpg 格式后,会发现依然是第一次拍摄的图片。

扫码模式

1. 将 SCAN_MODE 变量至 1,然后重新烧录程序。

local SCAN_MODE = 1 -- 写1演示扫码(使用摄像头对二维码、条形码或其他类型的图案进行扫描和识别)
local RAW_MODE = 0 -- 写1演示获取原始图像

2. 打开 https://cli.im/text 网址,生成一个二维码。

3. 我们按下 boot 按键,然后将摄像头对准二维码,即可打印二维码中的数据。

4. 我们再次按下 boot 按键,此时摄像头的扫码功能将会关闭,再将摄像头对准二维码不再打印二维码中的内容。

5.2.4 代码分析

LCD 显示摄像头数据

其原因是因为调用了如下函数。

camera.preview(camera_id, true) -- 打开LCD预览功能(直接将摄像头数据输出到LCD)
图片数据上传至电脑

1. 当我们按下 boot 按键,将会触发按键回调函数。此时会发送一个 "PRESS" 事件。

local function press_key()
    log.info("boot press")
    sys.publish("PRESS", true) -- 触发 "PRESS" 事件,并往特定topic通道发布一个消息
end
gpio.setup(0, press_key, gpio.PULLDOWN, gpio.RISING) -- 将 GPIO0 设置为输入模式,并设置上拉电阻和下降沿触发中断(boot按键)
gpio.debounce(0, 100, 1) -- GPIO0 按键防抖,防抖时长 100 ms,延时模式,中断后等待 100ms,期间若保持该电平了,时间到之后上报一次

2. 当出现了 "PRESS" 事件,如下函数的等待将能够达成,成功执行 if 语句。

result, data = sys.waitUntil("PRESS", 30*1000) -- 等待 "PRESS" 事件 30 秒(等待boot按键被按下)
if result == true and data == true then -- 如果 30 秒内检测到 "PRESS" 事件往下执行

3. 调用如下函数,让摄像头开始捕获数据,并且将摄像头的数据存入 "/testCamera.jpg" 文件中。

-- 将摄像头拍照的数据存入 "/testCamera.jpg" 文件中
-- jpeg压缩质量1最差,占用空间小,3最高,占用空间大,2和3需要非常多非常多的psram,尽量不要用
camera.capture(camera_id, "/testCamera.jpg", 1)

4. 当摄像头捕获数据完成,将会触发如下回调函数,因此此时日志信息中将会出现:"摄像头数据 xxxxx" 的内容。

camera.on(0, "scanned", function(id, str)
    if type(str) == 'string' then -- 如果是扫码模式(使用摄像头对二维码、条形码或其他类型的图案进行扫描和识别)
        log.info("扫码结果", str)
    elseif str == false then -- 如果摄像头没有正常工作
        log.error("摄像头没有数据")
    else -- 如果摄像头正常工作,并且不是扫码模式
        log.info("摄像头数据", str)
        sys.publish("capture done", true)
    end
end) -- 注册 camera 0 的 "scanned" 事件

5. 在如上的回调函数中将会发送 "capture done" 事件。此时如下函数任务达成,程序继续向下执行。

result, data = sys.waitUntil("capture done", 30000) -- 等待 "capture done" 事件 30s

6. 捕获到一帧数据后,马上停止摄像头捕获数据,同时创建一个任务线程将摄像头的数据通过串口传输给电脑,后续打印系统剩余内存。

camera.stop(camera_id) -- 暂停摄像头捕获数据
sendFile() -- 创建一个任务将摄像头数据通过串口发送到电脑

log.info("sys ram", rtos.meminfo("sys"))
log.info("lua ram", rtos.meminfo("psram"))
扫码模式

1. 因为 SCAN_MODE 值设置为 1。所以摄像头将会被设置为扫码模式。

local SCAN_MODE = 1 -- 写1演示扫码(使用摄像头对二维码、条形码或其他类型的图案进行扫描和识别)
camera_id = gc032aInit(cspiId, i2cId, 24000000, SCAN_MODE, SCAN_MODE) -- 通过 I2C1 配置摄像头,SPI1 时钟频率为 24 MHz

2. 当按下按键,条件满足,同时因为 scan_pause 初始值为 true。因此这里将会启动摄像头的扫码功能。

local scan_pause = true -- 扫码启动与停止标志位

log.info("启动扫码")
-- camera_id = gc0310Init(cspiId,i2cId,25500000,SCAN_MODE,SCAN_MODE)
camera.start(camera_id)
scan_pause = false
sys.wait(200)
log.info(rtos.meminfo("sys"))
log.info(rtos.meminfo("psram"))

3. 每次当摄像头检测到可解析的二维码时,将会触发如下回调函数,将二维码的信息打印出来。

camera.on(0, "scanned", function(id, str)
    if type(str) == 'string' then -- 如果是扫码模式(使用摄像头对二维码、条形码或其他类型的图案进行扫描和识别)
        log.info("扫码结果", str)
    elseif str == false then -- 如果摄像头没有正常工作
        log.error("摄像头没有数据")
    else -- 如果摄像头正常工作,并且不是扫码模式
        log.info("摄像头数据", str)
        sys.publish("capture done", true)
    end
end) -- 注册 camera 0 的 "scanned" 事件

4. 当我们再次按下 boot 按键,因为在上一次中启动扫码的,已经让 scan_pause 变量为 false。所以这一次执行的应该是停止扫码的代码。

-- 第一次启动扫码的代码,将 scan_pause = false
log.info("启动扫码")
-- camera_id = gc0310Init(cspiId,i2cId,25500000,SCAN_MODE,SCAN_MODE)
camera.start(camera_id)
scan_pause = false

-- 因此再次按下 boot 按键执行如下代码                    
log.info("停止扫码")
-- camera.close(camera_id)  --完全关闭摄像头才用这个
camera.stop(camera_id)
scan_pause = true
sys.wait(200)
log.info(rtos.meminfo("sys"))
log.info(rtos.meminfo("psram"))

六、总结

教程中详细解析了二进制数据在嵌入式系统中的处理机制,包括图像采集时的数据缓存管理、UART 传输协议的设计优化,以及如何通过 Lua 脚本实现高效的字节流操作。

实验提供了完整的 Lua 示例代码,涵盖摄像头驱动初始化、图像帧捕获、双路输出(UART+LCD)的协同控制。通过对比嵌入式端预览与计算机端还原图像的实践。本案例为物联网图像传输系统开发提供了典型参考,对理解边缘计算设备的图像处理全流程具有实践指导意义。

七、常见问题

7.1 使用 demo 报错

1. Luatools 要求 3_0_15 以上版本

2. 内核固件文件

7.2 捕获原始数据问题

SCAN_MODE = 0local RAW_MODE = 1 测试摄像头捕获原始数据的时候,将文件上传至电脑图片还有问题。解决办法:分段发送,每段传输 64k。

八、扩展

后续扩展在此补充,敬请期待......