跳转至

Can demo

作者:魏健强

一、CAN 概述

控制器局域网(Controller Area Network,CAN)是一种广泛应用于工业控制、汽车电子等领域的实时通信协议,由德国博世公司于 1986 年提出并标准化(ISO 11898)。其核心采用多主(Multi-Master)总线架构,通过差分信号传输实现高抗干扰能力,支持节点间以广播形式进行高效、可靠的数据交换。CAN 协议基于非破坏性仲裁机制,利用报文 ID 优先级解决总线冲突,确保高优先级数据实时传输,同时具备完善的错误检测、帧校验和故障节点自动关闭功能,满足严苛环境下的安全性需求。典型应用包括汽车 ECU 通信(如动力总成、车身控制)、工业自动化(如 PLC 联网)及医疗设备互联,其衍生协议如 CAN FD(灵活数据率)进一步提升了带宽与灵活性,成为现代分布式控制系统的基础技术之一。

二、演示功能概述

本篇文章演示的内容为:使用 Air780EPM 开发板使用 CAN 连接 CAN 转 USB 工具,进行数据收发

三、准备硬件环境

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

2.1 Air780EPM V1.3 开发板

2.2 高速 USB-CAN 分析仪:

购买链接

2.3 硬件连接图

Air780EPM开发板
USB_CAN调试工具
H
H
L
L
GND
GND

780EPM 开发板和 USB-CAN 分析仪链接图:

CAN 采用差分信号传输,通过两根信号线(CANH 和 CANL)传输数据。两根线上的信号相位相反,即使存在干扰,接收端也能通过差值还原出原始数据。所以 CAN 接线可以不接 GND

四、准备软件环境

1.烧录工具:Luatools 工具

2.本demo开发测试时使用的固件为LuatOS-SoC_V2018_Air780EPM,本demo对固件版本没有什么特殊要求,所以你如果要测试本demo时,可以直接使用最新版本的内核固件Air780EPM固件Air780EHM固件;如果发现最新版本的内核固件测试有问题,可以使用我们开发本demo时使用的内核固件版本来对比测试;

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

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

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

  1. PC 端的 USB_CAN 调试工具上位机软件;ZCANPRO 和 UCANFDtoCANFDNETTool

五、代码 API 和代码解析

5.1 代码 API

本教程使用 api 接口为:

https://docs.openluat.com/osapi/core/can/

https://docs.openluat.com/osapi/core/zbuff/

https://docs.openluat.com/osapi/core/mcu/#mcux32value

六、运行结果展示

6.1 CAN 总线正常模式使用示例

--[[
@module  can_normal
@summary CAN总线正常工作模式使用示例
@version 1.0
@date    2025.11.25
@author  魏健强
@usage
本文件为CAN总线正常工作模式使用示例,核心业务逻辑为:
1. 初始化CAN总线
2. 发送和接收CAN总线数据

本文件没有对外接口,直接在main.lua模块中require "can_normal"就可以加载运行;
]] 
local can_id = 0
local stb_pin = 28 -- 780EPM V1.3开发板上STB引脚为GPIO28
local rx_id = 0x12345677
local tx_id = 0x12345678
local test_cnt = 0
local tx_buf = zbuff.create(8) -- 创建zbuff
local send_queue = {} -- 发送队列
local MAX_SEND_QUEUE_LEN = 50 -- 发送队列最大长度
local send_res = false

-- 数据插入发送队列
local function can_send_data(id, msg_id, id_type, RTR, need_ack, data)
    if #send_queue >= MAX_SEND_QUEUE_LEN then
        log.error("can_send_data", "send queue full")
    end
    table.insert(send_queue, {
        id = id,
        msg_id = msg_id,
        id_type = id_type,
        RTR = RTR,
        need_ack = need_ack,
        data = data
    })
    sys.publish("CAN_SEND_DATA_EVENT")
end

local function can_cb(id, cb_type, param)
    if cb_type == can.CB_MSG then
        log.info("有新的消息")
        local succ, id, id_type, rtr, data = can.rx(id)
        while succ do
            log.info(mcu.x32(id), #data, data:toHex())
            succ, id, id_type, rtr, data = can.rx(id)
        end
    end
    if cb_type == can.CB_TX then
        if param then
            log.info("发送成功")
            send_res = true
        else
            log.info("发送失败")
            send_res = false
        end
        sys.publish("CAN_SEND_DATA_RES")
    end
    if cb_type == can.CB_ERR then
        -- param参数就是4字节错误码
        log.error("CAN错误", "错误码:", string.format("0x%08X", param))

        -- 解析错误码
        local direction = (param >> 16) & 0xFF -- byte2: 方向
        local error_type = (param >> 8) & 0xFF -- byte1: 错误类型  
        local position = param & 0xFF -- byte0: 错误位置

        -- 判断错误方向
        if direction == 0 then
            log.info("错误方向", "发送错误")
        else
            log.info("错误方向", "接收错误")
        end

        -- 判断错误类型
        if error_type == 0 then
            log.info("错误类型", "位错误")
        elseif error_type == 1 then
            log.info("错误类型", "格式错误")
        elseif error_type == 2 then
            log.info("错误类型", "填充错误")
        end

        -- 输出错误位置
        log.info("错误位置", string.format("0x%02X", position))
    end
    if cb_type == can.CB_STATE then
        -- 获取总线状态
        local state = can.state(can_id)
        log.info("can.state", "当前状态", state)
        -- 根据状态处理
        if state == can.STATE_ACTIVE then
            log.info("can.state", "总线正常")
        elseif state == can.STATE_PASSIVE then
            log.warn("can.state", "被动错误状态")
        elseif state == can.STATE_BUSOFF then
            log.error("can.state", "总线离线")
            -- 需要手动恢复
            can.reset(can_id)
        end
    end
end

local function can_tx_test(data)
    while true do
        sys.wait(10000)
        test_cnt = test_cnt + 1
        if test_cnt > 8 then
            test_cnt = 1
        end
        tx_buf:set(0, test_cnt) -- zbuff的类似于memset操作,类似于memset(&buff[start], num, len)
        tx_buf:seek(test_cnt) -- zbuff设置光标位置(可能与当前指针位置有关;执行后指针会被设置到指定位置)
        can_send_data(can_id, 0x123, can.STD, false, true, "Hello") -- 发送标准帧数据
        can_send_data(can_id, 0x123, can.STD, true, true, "") -- 发送遥控帧数据
        can_send_data(can_id, tx_id, can.EXT, false, true, tx_buf)--发送扩展帧数据
    end
end

-- can.debug(true)
-- gpio.setup(stb_pin,0)   -- 配置STB引脚为输出低电平
gpio.setup(stb_pin, 1) -- 780EPM V1.3开发板STB信号有逻辑取反,要配置成输出高电 
can.init(can_id, 128) -- 初始化CAN,参数为CAN ID,接收缓存消息数的最大值
can.on(can_id, can_cb) -- 注册CAN的回调函数
can.timing(can_id, 1000000, 6, 6, 4, 2) -- CAN总线配置时序

local num = 0x00f -- 接收屏蔽寄存器值,对应bit写0表示需要检测,写1表示不检测,默认是0xff ff ff ff,不过滤全接收
-- 接收消息过滤(以下四行代码四选一使用)
can.node(can_id, rx_id, can.EXT) -- 只接收消息id为rx_id的扩展帧数据
-- can.filter(can_id, false, 0x123 << 21, (num << 21) | 0x1FFFFF) -- 接收消息id为0x12开头的标准帧数据,0x120~0x12f,左移后接收屏蔽寄存器的值需要用1填充,所以 | 0x1FFFFF
-- can.filter(can_id, false, 0x12345678 << 3, (num << 3) | 0x7) -- 接收消息id为0x1234开头的扩展帧数据,0x12340000~0x1234ffff,左移后接收屏蔽寄存器的值需要用1填充,所以 | 0x7
-- can.filter(can_id, false, 0, 0xFFFFFFFF) -- 接收所有消息

-- 模式
can.mode(can_id, can.MODE_NORMAL) -- 一旦设置mode就开始正常工作了,此时不能再设置node,timing,filter等

local function send_task()
    local send_item
    local result, buff_full
    -- 遍历数据发送队列send_queue
    while true do
        sys.waitUntil("CAN_SEND_DATA_EVENT",1000)
        while #send_queue > 0 do
            send_res = false
            -- 取数据发送
            send_item = table.remove(send_queue, 1)
            while not send_res do
                can.tx(send_item.id, send_item.msg_id, send_item.id_type, send_item.RTR, send_item.need_ack,
                    send_item.data)
                sys.waitUntil("CAN_SEND_DATA_RES",500)
                -- 循环发送直到发送成功
            end
        end
    end
end
sys.taskInit(send_task)
sys.taskInit(can_tx_test)

6.2 CAN 总线自测模式使用示例;

--[[
@module  can_self_test
@summary CAN总线自测模式使用示例
@version 1.0
@date    2025.11.25
@author  魏健强
@usage
本文件为CAN总线自测模式使用示例,核心业务逻辑为:
1. 初始化CAN总线
2. 启用测试模式测试数据自发自收

本文件没有对外接口,直接在main.lua模块中require "can_self_test"就可以加载运行;
]] 
local can_id = 0
local stb_pin = 28 -- 780EPM V1.3开发板上STB引脚为GPIO28
local tx_id = 0x12345677

local test_cnt = 0
local tx_buf = zbuff.create(8)  --创建zbuff
local function can_cb(id, cb_type, param)
    if cb_type == can.CB_MSG then
        log.info("有新的消息")
        local succ, id, id_type, rtr, data = can.rx(id)
        while succ do
            log.info(mcu.x32(id), #data, data:toHex())
            succ, id, id_type, rtr, data = can.rx(id)
        end
    end
    if cb_type == can.CB_TX then
        if param then
            log.info("发送成功")
        else
            log.info("发送失败")
        end
    end
    if cb_type == can.CB_ERR then
        -- param参数就是4字节错误码
        log.error("CAN错误", "错误码:", string.format("0x%08X", param))

        -- 解析错误码
        local direction = (param >> 16) & 0xFF -- byte2: 方向
        local error_type = (param >> 8) & 0xFF -- byte1: 错误类型  
        local position = param & 0xFF -- byte0: 错误位置

        -- 判断错误方向
        if direction == 0 then
            log.info("错误方向", "发送错误")
        else
            log.info("错误方向", "接收错误")
        end

        -- 判断错误类型
        if error_type == 0 then
            log.info("错误类型", "位错误")
        elseif error_type == 1 then
            log.info("错误类型", "格式错误")
        elseif error_type == 2 then
            log.info("错误类型", "填充错误")
        end

        -- 输出错误位置
        log.info("错误位置", string.format("0x%02X", position))
    end
    if cb_type == can.CB_STATE then
        -- 获取总线状态
        local state = can.state(can_id)
        log.info("can.state", "当前状态", state)

        -- 根据状态处理
        if state == can.STATE_ACTIVE then
            log.info("can.state", "总线正常")
        elseif state == can.STATE_PASSIVE then
            log.warn("can.state", "被动错误状态")
        elseif state == can.STATE_BUSOFF then
            log.error("can.state", "总线离线")
            -- 需要手动恢复
            can.reset(can_id)
        end
    end
end

local function can_tx_test()
    log.info("can tx")
    test_cnt = test_cnt + 1
    if test_cnt > 8 then
        test_cnt = 1
    end
    tx_buf:set(0,test_cnt)  --zbuff的类似于memset操作,类似于memset(&buff[start], num, len)
    tx_buf:seek(test_cnt)   --zbuff设置光标位置(可能与当前指针位置有关;执行后指针会被设置到指定位置)
    can.tx(can_id, tx_id, can.EXT, false, true, tx_buf)
end
-- can.debug(true)
-- gpio.setup(stb_pin,0)   -- 配置STB引脚为输出低电平
gpio.setup(stb_pin, 1) -- 780EPM V1.3开发板STB信号有逻辑取反,要配置成输出高电 
can.init(can_id, 128)            -- 初始化CAN,参数为CAN ID,接收缓存消息数的最大值
can.on(can_id, can_cb)            -- 注册CAN的回调函数
can.timing(can_id, 1000000, 6, 6, 4, 2)     --CAN总线配置时序
can.node(can_id, tx_id, can.EXT)    -- 测试模式下,允许接收的ID和发送ID一致才会有新数据提醒
can.mode(can_id, can.MODE_TEST)     -- 如果只是自身测试硬件好坏,可以用测试模式来验证,如果发送成功就OK

sys.timerLoopStart(can_tx_test, 2000)

6.3 CAN 总线休眠模式使用示例;

--[[
@module  can_sleep
@summary CAN总线休眠模式使用示例
@version 1.0
@date    2025.11.25
@author  魏健强
@usage
本文件为CAN总线休眠模式使用示例,核心业务逻辑为:
1. 初始化CAN总线
2. 空闲时间进入休眠模式,定时唤醒发送数据

本文件没有对外接口,直接在main.lua模块中require "can_self_test"就可以加载运行;
]]
local can_id = 0
local stb_pin = 28 -- 780EPM V1.3开发板上STB引脚为GPIO28
local rx_id = 0x12345678
local tx_id = 0x12345677

local test_cnt = 0
local tx_buf = zbuff.create(8)  --创建zbuff
local function can_cb(id, cb_type, param)
    if cb_type == can.CB_MSG then
        log.info("有新的消息")
        local succ, id, id_type, rtr, data = can.rx(id)
        while succ do
            log.info(mcu.x32(id), #data, data:toHex())
            succ, id, id_type, rtr, data = can.rx(id)
        end
    end
    if cb_type == can.CB_TX then
        if param then
            log.info("发送成功")
        else
            log.info("发送失败")
        end
    end
    if cb_type == can.CB_ERR then
        -- param参数就是4字节错误码
        log.error("CAN错误", "错误码:", string.format("0x%08X", param))

        -- 解析错误码
        local direction = (param >> 16) & 0xFF -- byte2: 方向
        local error_type = (param >> 8) & 0xFF -- byte1: 错误类型  
        local position = param & 0xFF -- byte0: 错误位置

        -- 判断错误方向
        if direction == 0 then
            log.info("错误方向", "发送错误")
        else
            log.info("错误方向", "接收错误")
        end

        -- 判断错误类型
        if error_type == 0 then
            log.info("错误类型", "位错误")
        elseif error_type == 1 then
            log.info("错误类型", "格式错误")
        elseif error_type == 2 then
            log.info("错误类型", "填充错误")
        end

        -- 输出错误位置
        log.info("错误位置", string.format("0x%02X", position))
    end
    if cb_type == can.CB_STATE then
        -- 获取总线状态
        local state = can.state(can_id)
        log.info("can.state", "当前状态", state)

        -- 根据状态处理
        if state == can.STATE_ACTIVE then
            log.info("can.state", "总线正常")
        elseif state == can.STATE_PASSIVE then
            log.warn("can.state", "被动错误状态")
        elseif state == can.STATE_BUSOFF then
            log.error("can.state", "总线离线")
            -- 需要手动恢复
            can.reset(can_id)
        end
    end
end

local function can_tx_test()
    log.info("can tx")
    test_cnt = test_cnt + 1
    if test_cnt > 8 then
        test_cnt = 1
    end
    tx_buf:set(0,test_cnt)  --zbuff的类似于memset操作,类似于memset(&buff[start], num, len)
    tx_buf:seek(test_cnt)   --zbuff设置光标位置(可能与当前指针位置有关;执行后指针会被设置到指定位置)
    can.tx(can_id, tx_id, can.EXT, false, true, tx_buf)
end
-- can.debug(true)
-- gpio.setup(stb_pin,0)   -- 配置STB引脚为输出低电平
gpio.setup(stb_pin, 1) -- 780EPM V1.3开发板STB信号有逻辑取反,要配置成输出高电 
can.init(can_id, 128)            -- 初始化CAN,参数为CAN ID,接收缓存消息数的最大值
can.on(can_id, can_cb)            -- 注册CAN的回调函数
can.timing(can_id, 1000000, 6, 6, 4, 2)     --CAN总线配置时序
can.node(can_id, rx_id, can.EXT)    -- 设置过滤,只接收消息id为rx_id的扩展帧数据
can.mode(can_id, can.MODE_SLEEP)     -- 设置sleep模式

local function CAN_MODE_SLEEP()
    while true do
        sys.wait(1000)
        log.info("can_state", can.state(can_id))
        if can.state(can_id) == can.STATE_ACTIVE then
            can.mode(can_id, can.MODE_SLEEP)
        end
    end
end
sys.taskInit(CAN_MODE_SLEEP)
sys.timerLoopStart(can_tx_test, 10000)

6.4 main 主程序入口

--[[
@module  main
@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑 
@version 1.0
@date    2025.11.25
@author  魏健强
@usage
本demo演示的核心功能为:
演示can功能的使用:
1. can正常工作模式
2. can自测模式,自发自收
3. can休眠模式
]]
--[[
必须定义PROJECT和VERSION变量,Luatools工具会用到这两个变量,远程升级功能也会用到这两个变量
PROJECT:项目名,ascii string类型
        可以随便定义,只要不使用,就行
VERSION:项目版本号,ascii string类型
        如果使用合宙iot.openluat.com进行远程升级,必须按照"XXX.YYY.ZZZ"三段格式定义:
            X、Y、Z各表示1位数字,三个X表示的数字可以相同,也可以不同,同理三个Y和三个Z表示的数字也是可以相同,可以不同
            因为历史原因,YYY这三位数字必须存在,但是没有任何用处,可以一直写为000
        如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
]]-- Luatools需要PROJECT和VERSION这两个信息
PROJECT = "can"
VERSION = "001.000.000"

-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)

-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
if wdt then
    --配置喂狗超时时间为9秒钟
    wdt.init(9000)
    --启动一个循环定时器,每隔3秒钟喂一次狗
    sys.timerLoopStart(wdt.feed, 3000)
end

-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
-- 启动errDump日志存储并且上传功能,600秒上传一次
-- if errDump then
--     errDump.config(true, 600)
-- end

-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
-- 可以使用合宙的iot.openluat.com平台进行远程升级
-- 也可以使用客户自己搭建的平台进行远程升级
-- 远程升级的详细用法,可以参考fota的demo进行使用

-- 启动一个循环定时器
-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
-- 方便分析内存使用是否有异常
-- sys.timerLoopStart(function()
--     log.info("mem.lua", rtos.meminfo())
--     log.info("mem.sys", rtos.meminfo("sys"))
-- end, 3000)

gpio.setup(23, 1) -- 要手动打开,否则无法使用CAN芯片不能正常工作

require "can_normal"
-- require "can_self_test"
-- require "can_sleep"

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

6.2 结果展示

can 总线正常模式,发送标准帧,扩展帧,遥控帧。接收扩展帧数据。

can 自测模式,自发自收数据

Can 空闲时为休眠模式,接收数据或者发送数据时为正常模式。

七、总结

本文演示如何在 Air780EPM 开发板上面,用 CAN 接口,使用 USB 转 CAN 工具进行数据的收发.

八、硬件电路说明

合宙 Air780Exx 系列模组管脚详细说明