一、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 信号:
- 电源管理(待机模式控制)
低功耗模式:当系统需要进入节能状态时(如汽车熄火或设备待机),CAN_STB 信号可被触发(高电平或低电平,取决于硬件设计),使 CAN 收发器进入低功耗待机模式。此时,收发器停止正常通信以降低能耗。
唤醒功能:当需要恢复通信时,CAN_STB 信号状态切换(如拉低或拉高),将收发器从待机模式唤醒,重新激活 CAN 总线的数据传输。
- 硬件控制
收发器启用/禁用:在某些 CAN 收发器芯片(如 TI 的 SN65HVD230)中,STB(Standby)引脚直接控制收发器的工作状态。例如:
STB = 高电平:收发器关闭,仅消耗微量静态电流。
STB = 低电平:收发器正常工作,可收发 CAN 信号。
系统集成:在复杂系统中,CAN_STB 可能由主控制器(如 MCU)输出,协调多个 CAN 节点的电源状态,优化整体能耗。
- 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 拉低。