跳转至

一、CAN 概述

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

二、演示功能概述

本篇文章演示的内容为:用两种方式测试 780EPM 的 CAN 功能,第一种是使用 Air780EPM 开发板使用 CAN 连接 CAN 转 USB 工具,进行数据收发,第二种是使用 780EPM 开发板和 780EPM 开发板进行 CAN 功能的收发测试

三、准备硬件环境

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

2.1 Air780EPM V1.3 开发板

2.2 高速 USB-CAN 分析仪:购买链接

2.3 硬件连接图

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

CAN 总线采用差分信号传输,由两根线组成:

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

780EPM 开发板和 780EPM 开发板连接图:

四、准备软件环境

烧录工具:Luatools 工具

Air780EPM 烧录需要的固件和脚本文件:

LuatOS 运行所需要的 lib 文件:

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

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

五、代码 API 和代码解析

5.1 代码 API

can.init(id, rx_message_cache_max)

CAN 总线初始化

参数

传入值类型
解释
int
id, 如果只有一条总线写0或者留空, 有多条的,can0写0,can1写1, 如此类推, 一般情况只有1条
int
rx_message_cache_max,接收缓存消息数的最大值,写0或者留空则使用平台默认值

返回值

返回值类型
解释
boolean
成功返回true,失败返回false

例子

can.init(0)

can.on(id, func)

注册 CAN 事件回调

参数

传入值类型
解释
int
id, 如果只有一条总线写0或者留空, 有多条的,can0写0,can1写1, 如此类推, 一般情况只有1条
function
回调方法

返回值

返回值类型
解释
nil
无返回值

例子

can.on(1, function(id, type, param)
    log.info("can", id, type, param)
end)

can.timing(id, br, PTS, PBS1, PBS2, SJW)

CAN 总线配置时序

参数

传入值类型
解释
int
id, 如果只有一条总线写0或者留空, 有多条的,can0写0,can1写1, 如此类推, 一般情况只有1条
int
br, 波特率, 默认1Mbps
int
PTS, 传播时间段, 范围1~8,默认5
int
PBS1, 相位缓冲段1,范围1~8,默认4
int
PBS2, 相位缓冲段2,范围2~8,默认3
int
SJW, 同步补偿宽度值,范围1~4,默认2

返回值

返回值类型
解释
boolean
成功返回true,失败返回false

例子

can.timing(0, 1000000, 5, 4, 3, 2)
can.timing(0, 650000, 9, 6, 4, 2)
can.timing(0, 500000, 5, 4, 3, 2)
can.timing(0, 250000, 5, 4, 3, 2)
can.timing(0, 125000, 5, 4, 3, 2)
can.timing(0, 100000, 5, 4, 3, 2)
can.timing(0, 50000, 9, 6, 4, 2)
can.timing(0, 25000, 9, 6, 4, 2)

can.mode(id, mode)

CAN 总线设置工作模式

参数

传入值类型
解释
int
id, 如果只有一条总线写0或者留空, 有多条的,can0写0,can1写1, 如此类推, 一般情况只有1条
int
mode, 见MODE_XXX,默认是MODE_NORMAL

返回值

返回值类型
解释
boolean
成功返回true,失败返回false

例子

can.mode(0, CAN.MODE_NORMAL)

can.node(id, node_id, id_type)

CAN 总线设置节点 ID,这是一种简易的过滤规则,只接收和 ID 完全匹配的消息,和 can.filter 选择一个使用

参数

传入值类型
解释
int
id, 如果只有一条总线写0或者留空, 有多条的,can0写0,can1写1, 如此类推, 一般情况只有1条
int
node_id, 节点ID, 标准格式11位或者扩展格式29位,根据id_type决定,默认值是0x1fffffff,id值越小,优先级越高
int
id_type,ID类型,填1或者CAN.EXT为扩展格式,填0或者CAN.STD为标准格式

返回值

返回值类型
解释
boolean
成功返回true,失败返回false

例子

can.node(0, 0x12345678, CAN.EXT)
can.node(0, 0x123, CAN.STD)

can.tx(id, msg_id, id_type, RTR, need_ack, data)

CAN 发送一条消息

参数

传入值类型
解释
int
id, 如果只有一条总线写0或者留空, 有多条的,can0写0,can1写1, 如此类推, 一般情况只有1条
int
msg_id, 节点ID, 标准格式11位或者扩展格式29位,根据id_type决定,默认值是0x1fffffff,id值越小,优先级越高
int
id_type, ID类型,填1或者CAN.EXT为扩展格式,填0或者CAN.STD为标准格式
boolean
RTR, 是否是遥控帧,true是,false不是,默认是false
boolean
need_ack,是否需要应答,true是,false不需要,默认是true
string/zbuff
data, 需要发送的数据, 如果是zbuff会从指针起始位置开始发送,最多发送8字节

返回值

返回值类型
解释
boolean
成功返回true,失败返回false

例子

can.tx(id, 0x12345678, CAN.EXT, false, true, "\x00\x01\x02\x03\0x04\x05\0x6\x07")

can.rx(id)

从缓存里读出一条消息

参数

传入值类型
解释
int
id, 如果只有一条总线写0或者留空, 有多条的,can0写0,can1写1, 如此类推, 一般情况只有1条

返回值

返回值类型
解释
boolean
是否读出数据,true读出,false没有读出,缓存已经空了,或者id不对
int
消息ID
int
ID类型,1或者CAN.EXT为扩展格式,0或者CAN.STD为标准格式
boolean
是否是遥控帧,true是,false不是
string
数据

例子

local succ, id, type, rtr, data = can.rx(0)

can.debug(on_off)

CAN debug 开关,打开后有更详细的打印

参数

传入值类型
解释
boolean
true打开,false关闭
return
nil

返回值

例子

can.debug(true)

zbuff.create(length,data,type)

创建 zbuff

参数

传入值类型
解释
int
字节数
any
可选参数,number时为填充数据,string时为填充字符串
number
可选参数,内存类型,可选:zbuff.HEAP_SRAM(内部sram,默认) zbuff.HEAP_PSRAM(外部psram) zbuff.HEAP_AUTO(自动申请,如存在psram则在psram进行申请,如不存在或失败则在sram进行申请) 注意:此项与硬件支持有关

返回值

返回值类型
解释
object
zbuff对象,如果创建失败会返回nil

例子

-- 创建zbuff
local buff = zbuff.create(1024) -- 空白的
local buff = zbuff.create(1024, 0x33) --创建一个初值全为0x33的内存区域
local buff = zbuff.create(1024, "123321456654") -- 创建,并填充一个已有字符串的内容

-- 创建framebuff用的zbuff
-- zbuff.create({width,height,bit},data,type)
-- table 宽度、高度、色位深度
@int 可选参数,填充数据
@number 可选参数,内存类型,可选:zbuff.HEAP_SRAM(内部sram,默认) zbuff.HEAP_PSRAM(外部psram) zbuff.HEAP_AUTO(自动申请,如存在psram则在psram进行申请,如不存在或失败则在sram进行申请) 注意:此项与硬件支持有关
@return object zbuff对象,如果创建失败会返回nil
@usage
-- 创建zbuff
local buff = zbuff.create({128,160,16})--创建一个128*160的framebuff
local buff = zbuff.create({128,160,16},0xf800)--创建一个128*160的framebuff,初始状态红色

buff:set(start, num, len)

zbuff 的类似于 memset 操作,类似于 memset(&buff[start], num, len),当然有 ram 越界保护,会对 len 有一定的限制

参数

传入值类型
解释
int
可选,开始位置,默认为0,
int
可选,默认为0。要设置为的值
int
可选,长度,默认为全部空间,如果超出范围了,会自动截断

返回值

例子

-- 全部初始化为0
buff:set() --等同于 memset(buff, 0, sizeof(buff))
buff:set(8) --等同于 memset(&buff[8], 0, sizeof(buff) - 8)
buff:set(0, 0x55) --等同于 memset(buff, 0x55, sizeof(buff))
buff:set(4, 0xaa, 12) --等用于 memset(&buff[4], 0xaa, 12)

buff:seek(base,offset)

zbuff 设置光标位置(可能与当前指针位置有关;执行后指针会被设置到指定位置)

参数

传入值类型
解释
int
偏移长度
int
where, 基点,默认zbuff.SEEK_SET。zbuff.SEEK_SET: 基点为 0 (文件开头),zbuff.SEEK_CUR: 基点为当前位置,zbuff.SEEK_END: 基点为文件尾

返回值

返回值类型
解释
int
设置光标后从buff开头计算起的光标的位置

例子

buff:seek(0) -- 把光标设置到指定位置
buff:seek(5,zbuff.SEEK_CUR)
buff:seek(-3,zbuff.SEEK_END)

mcu.x32(value)

转换 10 进制数为 16 进制字符串输出

参数

传入值类型
解释
int
需要转换的值

返回值

返回值类型
解释
string
16进制字符串

例子

local value = mcu.x32(0x2009FFFC) --输出"0x2009fffc"

5.2 常量值解释

常量
类型
解释
can.MODE_NORMAL
number
正常工作模式
can.MODE_LISTEN
number
监听模式
can.MODE_TEST
number
自测模式
can.MODE_SLEEP
number
休眠模式
can.STATE_STOP
number
停止工作状态
can.STATE_ACTIVE
number
主动错误状态,一般都是这个状态
can.STATE_PASSIVE
number
被动错误状态,总线上错误多会进入这个状态,但是还能正常收发
can.STATE_BUSOFF
number
离线状态,总线上错误非常多会进入这个状态,不能收发,需要手动退出
can.STATE_LISTEN
number
监听状态,选择监听模式时进入这个状态
can.STATE_TEST
number
自收自发状态,选择自测模式时进入这个状态
can.STATE_SLEEP
number
休眠状态,选择休眠模式时进入这个状态
can.CB_MSG
number
回调消息类型,有新数据写入缓存
can.CB_TX
number
回调消息类型,数据发送结束,需要根据后续param确定发送成功还是失败
can.CB_ERR
number
回调消息类型,有错误报告,后续param是错误码
can.CB_STATE
number
回调消息类型,总线状态变更,后续param是新的状态,也可以用can.state读出
can.EXT
number
扩展帧
can.STD
number
标准帧

5.3 代码解析

1.配置 SELF_TEST_FLAG 为正常收发模式,节点配置为 A 节点,对应的 rx 的 id 为 0x12345678,tx 的 id 为 0x12345677,代码里面使用的 id 为扩展帧,扩展帧和标准帧的区别:

CAN 协议中标准帧和扩展帧的主要区别体现在以下几个方面:

一、标识符长度与节点数量

1.标准帧

2.扩展帧

二、数据传输能力

标准帧数据长度限制为 8 字节,超过部分会被填充或忽略。

扩展帧数据长度可达 0-64 字节,通过分段传输(如 ISO-TP 协议)实现大容量数据传输。

三、控制字段差异

标准帧:6 位控制字段(如 DLC、R0、IDE 位)。

扩展帧:4 位控制字段(如 DLC、R1、IDE 位),部分功能由扩展标识符扩展。

配置了 STB 的 pin 用于不同硬件的控制,创建了 Zbuff 存储发送数据

local SELF_TEST_FLAG = false --自测模式标识,写true就进行自收自发模式,写false就进行正常收发模式
local node_a = true   -- A节点写true, B节点写false
local can_id = 0
local rx_id
local tx_id
local stb_pin = 28      -- stb引脚根据实际情况写,不用的话,也可以不写
if node_a then          -- A/B节点区分,互相传输测试
    rx_id = 0x12345678
    tx_id = 0x12345677
else
    rx_id = 0x12345677
    tx_id = 0x12345678
end
local test_cnt = 0
local tx_buf = zbuff.create(8)  --创建zbuff

2.初始化 CAN 总线,设置 id 和接收缓存消息数,注册 CAN 事件的回调函数,对 cb_type 进行对比,can.CB_MSG 为对有新数据写入缓存进行读取处理,can.CB_TX 为数据发送结束,需要根据后续 param 确定发送成功还是失败,can.CB_ERR 为有错误报告,后续 param 是错误码,can.CB_STATE 为总线状态变更,后续 param 是新的状态,也可以用 can.state 读出,然后对 can 总线的时序进行配置,代码里配置的是 1Mbps,然后根据上面的是否是自测模式的变量进行判断,如果是自测模式就使用 can.mode 配置 can 总线的工作模式为自测模式 can.MODE_TEST,如果是正常工作模式就配置为 can.MODE_NORMAL,本文档中使用的为扩展帧,配置节点 ID 为 CAN.EXT,如果要使用标准帧配置为 CAN.STD,其中 STB 的管脚,

关于 CAN_STB 信号:

  1. 电源管理(待机模式控制)

低功耗模式:当系统需要进入节能状态时(如汽车熄火或设备待机),CAN_STB 信号可被触发(高电平或低电平,取决于硬件设计),使 CAN 收发器进入低功耗待机模式。此时,收发器停止正常通信以降低能耗。

唤醒功能:当需要恢复通信时,CAN_STB 信号状态切换(如拉低或拉高),将收发器从待机模式唤醒,重新激活 CAN 总线的数据传输。

  1. 硬件控制

收发器启用/禁用:在某些 CAN 收发器芯片(如 TI 的 SN65HVD230)中,STB(Standby)引脚直接控制收发器的工作状态。例如:

STB = 高电平:收发器关闭,仅消耗微量静态电流。

STB = 低电平:收发器正常工作,可收发 CAN 信号。

系统集成:在复杂系统中,CAN_STB 可能由主控制器(如 MCU)输出,协调多个 CAN 节点的电源状态,优化整体能耗。

  1. Air780EPM 开发板设计

注意!Air780EPM 开发板,为了电平转换的需要,在 Air780EPM 侧,CAN_STB 信号,实际需要作如下反向设计:

STB = 低电平:收发器关闭,仅消耗微量静态电流。

STB = 高电平:收发器正常工作,可收发 CAN 信号。

可根据设计来拉高或者拉低 STB 脚

local stb_pin = 28      -- stb引脚根据实际情况写,不用的话,也可以不写
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
        log.info("CAN错误码", mcu.x32(param))
    end
    if cb_type == can.CB_STATE then
        log.info("CAN新状态", param)
    end
end

gpio.setup(stb_pin,0)   -- 配置STB引脚为输出低电平
-- gpio.setup(stb_pin,1)    -- 如果开发板上STB信号有逻辑取反,则要配置成输出高电平
can.init(can_id, 128)            -- 初始化CAN,参数为CAN ID,接收缓存消息数的最大值
can.on(can_id, can_cb)            -- 注册CAN的回调函数
can.timing(can_id, 1000000, 5, 4, 3, 2)     --CAN总线配置时序
if SELF_TEST_FLAG then
    can.node(can_id, tx_id, can.EXT)    -- 测试模式下,允许接收的ID和发送ID一致才会有新数据提醒
    can.mode(can_id, can.MODE_TEST)     -- 如果只是自身测试硬件好坏,可以用测试模式来验证,如果发送成功就OK
else
    can.node(can_id, rx_id, can.EXT)
    can.mode(can_id, can.MODE_NORMAL)   -- 一旦设置mode就开始正常工作了,此时不能再设置node,timing,filter等
end

3.设置循环定时器,2 秒钟发一次数据,根据节点判断是 A 节点还是 B 节点打印节点发送信息,设置 test_cnt 计数器,每发送一次 +1,>8 的时候,设置为 1,然后使用 tx_buf:set(0,test_cnt)函数,把 buff 里面的内容设置成从索引 0 开始,连续 8 个字节被填充为 test_cnt,因为 zbuff 创建空间位 8,所以是 8 个字节,然后利用 tx_buf:seek 把光标位置设置为 test_cnt 的值,比如:填充完之后内容为 01 01 01 01 01 01 01 01,设置第一个光标的值设置完为 01,设置第二个光标为 01 01,然后利用 can.tx 发送内容,其中有涉及到是否需要应答的机制,can.tx 的第五个参数,need_ack

其中该机制为:

CAN 协议中的应答机制是其核心可靠性保障措施之一,主要通过以下方式实现:

一、应答机制的核心组成

ACK 应答位

应答间隙(ACK SLOT)

应答界定符(ACK DELIMITER)

二、应答机制的工作流程

数据传输

发送节点发送数据帧后进入等待应答状态,总线进入仲裁阶段。

仲裁与应答

错误处理

local test_cnt = 0
local function can_tx_test(data)
    if node_a then
        log.info("node a tx")
    else
        log.info("node b tx")
    end
    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

sys.timerLoopStart(can_tx_test, 2000)

六、运行结果展示

6.1 完整代码

PROJECT = "candemo"
VERSION = "1.0.0"
sys = require("sys")
log.style(1)
local SELF_TEST_FLAG = false --自测模式标识,写true就进行自收自发模式,写false就进行正常收发模式
local node_a = true   -- A节点写true, B节点写false
local can_id = 0
local rx_id
local tx_id
local stb_pin = 28      -- stb引脚根据实际情况写,不用的话,也可以不写
if node_a then          -- A/B节点区分,互相传输测试
    rx_id = 0x12345678
    tx_id = 0x12345677
else
    rx_id = 0x12345677
    tx_id = 0x12345678
end
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
        log.info("CAN错误码", mcu.x32(param))
    end
    if cb_type == can.CB_STATE then
        log.info("CAN新状态", param)
    end
end

local function can_tx_test(data)
    if node_a then
        log.info("node a tx")
    else
        log.info("node b tx")
    end
    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)    -- 如果开发板上STB信号有逻辑取反,则要配置成输出高电平
can.init(can_id, 128)            -- 初始化CAN,参数为CAN ID,接收缓存消息数的最大值
can.on(can_id, can_cb)            -- 注册CAN的回调函数
can.timing(can_id, 1000000, 5, 4, 3, 2)     --CAN总线配置时序
if SELF_TEST_FLAG then
    can.node(can_id, tx_id, can.EXT)    -- 测试模式下,允许接收的ID和发送ID一致才会有新数据提醒
    can.mode(can_id, can.MODE_TEST)     -- 如果只是自身测试硬件好坏,可以用测试模式来验证,如果发送成功就OK
else
    can.node(can_id, rx_id, can.EXT)
    can.mode(can_id, can.MODE_NORMAL)   -- 一旦设置mode就开始正常工作了,此时不能再设置node,timing,filter等
end

sys.timerLoopStart(can_tx_test, 2000)
sys.run()

6.2 结果展示

使用两个 780EPM 开发板测试结果:

一个开发板为节点 A,一个开发板为节点 B,互相发送数据接收对方的数据

七、总结

本文演示如何在 Air780EPM 开发板上面,用 CAN 接口,使用 USB 转 CAN 工具进行数据的收发,然后使用 780EPM 和 780EPM 两个开发板把 CAN 接口进行互连,进行双方数据的互发互收的演示。

八、常见问题

1.如何判断模块是否正常,如何自测?

如果有 CAN 收发器的情况下,直接设置为 can.mode(can_id, can.MODE_TEST)测试然后日志提示发送成功,则表示模块端的 CAN 接口功能均正常。如果发送失败,则需要短接模块的 CAN_TX 和 CAN_RX,测试是否发送正常,如果发送正常,证明模块这两个脚是是没有问题的,测量下 STB 的电平是否是低,如果为高,则需要代码里面设置对应的 gpio 拉低。

四、硬件电路说明

Air780EPM CAN 硬件电路说明