DeviceNet从站实现:ADμC812+SJA1000完整工程解析
2026/9/16 14:16:29 网站建设 项目流程

简介:这是一份面向工业自动化与嵌入式开发者的DeviceNet从站实现方案,基于ADI ADμC812单片机,完整覆盖CAN控制器配置、中断收发、SDO/PDO服务与对象字典等关键环节,适合需要快速掌握DeviceNet从站协议栈搭建和调试的工程师,也可作为同类CAN单片机移植的起点。压缩包内共43个文件,包含C源码、头文件、Keil工程文件、启动汇编、链接脚本以及编译生成的lst、obj、hex等,能够还原完整开发、编译与烧录流程;学习时可按工程文件、源码、说明文档与编译输出分层浏览,整体大小283KB。代码中协议定义、CAN驱动、定时器及用户主程序模块划分清晰,便于结合规范逐层学习;hex文件可配合硬件直接验证从站通信效果,说明文档和备份工程也有助于排查初始化与报文处理问题。目前已有484人浏览学习,是从站开发初期值得对照的实用模板。

1. 从站不是凭空写出来的:一套能跑的 DeviceNet 从站工程

从工程文件清单来看,DEVICENET_ADC.hex、DeviceNet.c、SJA1000.C、ADuC812.h、timer.c、User.c 全部都在,这是一套完整的 Keil C51 工程。很多人搜“devicenet_单片机”,实际想找的就是这种现成可复现的协议栈,而不是从零开始啃一百多页 ODVA 规范。这里的关键选型是 ADμC812 加上外部 CAN 控制器 SJA1000。ADμC812 是 8051 内核的混合信号单片机,负责对象字典、状态机和用户业务,SJA1000 负责 CAN 帧的物理收发。这个组合在 51 单片机生态里很常见,也顺带解释了为什么工程里会有 SJA1000.C 这个文件。这个工程适合三类人:一是要把现场设备接入 DeviceNet 网络的嵌入式工程师,二是要做控制器局域网相关课程设计或毕业设计的学生,三是想弄懂 CAN 应用层协议怎么从零搭建的开发者。它直接给出了对象字典、SJA1000 驱动、定时器驱动和用户回调的最小实现,照着跑通比空谈协议有意义得多。

2. DeviceNet 从站协议的骨架:对象字典、SDO 与报文组

DeviceNet 从站协议层的工作方式和很多人想象中不一样:它不要求从站处理网络上的所有报文,也不需要完整实现协议栈的全部细节。从站只要把自己关心的对象字典暴露出来,把显式报文和 I/O 报文分发到对应处理函数,就足够通过主站扫描和基本读写测试。这份工程里的 DeviceNet_def.h 和 User.c 分别承担对象字典定义和用户回调,DeviceNet.c 负责报文解析和状态机。下面拆开看。

2.1 对象字典是从站的“内存地图”

DeviceNet 从站本质上是“通过 CAN 总线暴露出来的一组对象”。每个对象由 class_id、instance、attr 三个维度定位。例如 Identity 对象的 class_id 是 0x01,实例号固定 0x01,属性 1 是 VendorID,属性 2 是 ProductType。主站读从站身份信息,实际就是发送一条 Get_Attribute 显式报文,CAN 数据域里携带这三个维度和属性号。对象字典表就是把这些定位信息映射到一个处理函数指针。

常见做法是维护一张静态表,而不是写一长串 if-else。我一般会定义下面的结构体,把对象字典表的每一项做成一个入口。

/* 对象字典项 */ typedef struct { uint16_t class_id; /* DeviceNet 对象类,0x01=Identity */ uint8_t instance; /* 实例号,从 0x01 开始 */ uint8_t attr; /* 属性号,1=VendorID */ uint8_t service; /* 服务码:0x0E=Get, 0x10=Set */ void (*handler)(void); /* 对应的回调函数 */ } OD_ENTRY; OD_ENTRY od_table[] = { {0x01, 0x01, 0x01, 0x0E, identity_vendor_id_get}, {0x01, 0x01, 0x02, 0x0E, identity_product_type_get}, {0x04, 0x01, 0x03, 0x10, assembly_data_set}, {0x04, 0x01, 0x03, 0x0E, assembly_data_get}, };

这段代码的逻辑是:收到显式请求后,先按 class_id、instance、attr 去查 od_table,匹配后再校验 service 是否一致。命中就调用 handler,未命中则向主站回 0x0B(服务不支持)。这样新增一个对象只需要在表里追加一行,DeviceNet.c 的协议解析不动。参数中的 service 是 DeviceNet 标准服务码,Get_Attribute 是 0x0E,Set_Attribute 是 0x10,Assembly 对象类 ID 是 0x04,实例和属性号要根据现场设备定义。

2.2 SDO/PDO 和 DeviceNet 的对应关系

熟悉 CANopen 的读者肯定知道 SDO 和 PDO,DeviceNet 没有直接使用这两个缩写,它的显式报文承担了 SDO 的职责,I/O 报文承担了 PDO 的职责。这个对应关系很重要,因为不少人把 DeviceNet 的读写请求当成 I/O 报文处理,结果一直收不到预期数据。显式报文用于配置、诊断和参数读写,长度可以跨多个 CAN 帧,需要分片重组。I/O 报文则是固定的 8 字节以内数据,直接映射到 Assembly 对象,不做分片处理。

从站侧需要处理的报文可以归纳成下面这个简表,完整划分要查 ODVA 对 11 位 CAN ID 的定义。

报文类型CAN ID 范围触发方式典型用途
重复 MAC ID 检测0x3FF上电广播一次检查网络中是否有同名节点
UCMM 未连接显式报文0x3F6~0x3FA主站发起连接建立、设备扫描
组 1 I/O 报文0x200~0x2FF主站轮询/中断实时输入输出数据

这张表的意义在于调试时先看报文落在哪个范围。如果主站扫描工具一直发 0x3F6 而板子没有任何反应,说明 UCMM 通道没有打开,问题大概率在协议层的状态机,而不是 SJA1000 驱动。反过来,如果 I/O 报文发过去了但从站不回,就要去查 Assembly 对象的映射是否完成。

2.3 分片与缓冲:显式报文不是一帧解决

显式报文的数据域第一个字节是控制字节,位 0 是 Fragment 位,位 1 是应答请求位,位 2-3 是报文 ID,位 5-6 组合表示后续分片。一个 8 字节 CAN 帧最多承载 7 字节有效数据,所以读写较长的对象属性时要分片发送。工程里 DeviceNet.c 要维护一个报文组装缓冲区和序列号,收到分片后按顺序拼接,全部到齐再交给对象字典处理。超时要及时清空缓冲区,我一般用 20ms 无后续分片作为超时阈值。

另一点需要注意的是,I/O 报文和显式报文的接收缓冲区必须分开管理,否则在运行状态下,轮询 I/O 的响应会被显式报文的分片重组打断。用中断里只写接收队列、主循环里再分流的做法,能避免中断嵌套带来的数据竞争。51 单片机资源有限,接收队列长度可以定为 8 帧,超出就丢弃并置溢出标志。

3. ADμC812 与 SJA1000 的通信层:初始化参数与中断收发

3.1 为什么是 SJA1000,而不是片内 CAN

ADI 的 ADμC812 是 8051 核的混合信号单片机,集成了高精度 ADC、DAC、Flash 和 UART,但本身并没有 CAN 控制器。工程里出现的 SJA1000.C 正是用来补这个缺口的。SJA1000 是经典的独立 CAN 控制器,并行接口可以直接挂在单片机的数据/地址总线上,对 51 系列来说比 SPI 接口的 MCP2515 更好迁移,也比换一颗 STM32 单片机来得直接。

实际连接方式是:SJA1000 的 AD0~AD7 接 ADμC812 的 P0 口,ALE 接单片机的 ALE 或由译码逻辑产生,片选 CS 接到外部存储区的高位地址线。这样 SJA1000 的寄存器就被映射到 ADμC812 的外部数据空间。注意 ADμC812 的外部总线时序要设置成允许比较慢的外部器件,否则 SJA1000 的寄存器读写会偶发失败。

3.2 复位模式与寄存器映射

SJA1000 上电后处于复位模式,所有配置寄存器在复位模式下才能写入。判断复位模式是否生效,不能只写寄存器,还要把 CAN_MOD 读回来确认。下面是我常用的初始化函数。

#define CAN_MOD 0x00 #define CAN_CMR 0x01 #define CAN_IR 0x03 #define CAN_BTR0 0x06 #define CAN_BTR1 0x07 #define CAN_OCR 0x08 #define CAN_ACR0 0x09 #define CAN_AMR0 0x0D #define CAN_CDR 0x1F /* SJA1000 片选映射到 ADμC812 外部数据空间 */ #define SJA_BASE ((volatile unsigned char xdata *)0x8000) void sja_init(unsigned char btr0, unsigned char btr1) { unsigned char tmp; SJA_BASE[CAN_MOD] = 0x01; /* 请求进入复位模式 */ tmp = SJA_BASE[CAN_MOD]; /* 读回模式寄存器 */ if ((tmp & 0x01) == 0) { return; /* 复位模式未生效,中止 */ } SJA_BASE[CAN_CDR] = 0xC8; /* PeliCAN,禁止 CLKOUT */ SJA_BASE[CAN_OCR] = 0xFA; /* 输出控制:正常输出 */ SJA_BASE[CAN_ACR0] = 0x00; /* 验收代码 0 */ SJA_BASE[CAN_ACR0+1] = 0x00; /* ACR1 */ SJA_BASE[CAN_ACR0+2] = 0x00; /* ACR2 */ SJA_BASE[CAN_ACR0+3] = 0x00; /* ACR3 */ SJA_BASE[CAN_AMR0] = 0xFF; /* 验收屏蔽:全部接收 */ SJA_BASE[CAN_AMR0+1] = 0xFF; SJA_BASE[CAN_AMR0+2] = 0xFF; SJA_BASE[CAN_AMR0+3] = 0xFF; SJA_BASE[CAN_BTR0] = btr0; /* 波特率预分频 */ SJA_BASE[CAN_BTR1] = btr1; /* 采样点配置 */ SJA_BASE[CAN_MOD] = 0x00; /* 退出复位模式 */ }

这里有两个容易被忽略的细节。第一,SJA1000 的寄存器地址在复位模式和正常模式下是复用的,比如 AMR3 和前导接收缓冲区共享地址,所以一定要按“复位模式先写全部配置,再清 MOD.0”这个顺序来。第二,验收屏蔽初始化全 0xFF 表示所有报文都收,联调时问题定位最方便;正式上线再把屏蔽码收紧到本节点相关的组 1 和组 2 报文。参数 btr0、btr1 的具体取值见下一节。

3.3 波特率参数选择

SJA1000 需要外部晶体,这里按常见的 16MHz 计算。DeviceNet 要求波特率 125kbps、250kbps、500kbps,其中 125kbps 是默认值。SJA1000 一个 bit 时间等于同步段加 TSEG1 加 TSEG2,最终波特率由预分频和这几个段共同决定。

波特率BTR0BTR1预分频采样点
125kbps0x070x1C887.5%
250kbps0x030x1C487.5%
500kbps0x010x1C287.5%

我一般先用 125kbps 联调,因为 DeviceNet 的默认波特率就是 125k,且对线缆质量要求最低。BTR0 的低 6 位是预分频值减 1,所以 0x07 代表实际分频 8;BTR1 的 TSEG1 取 13、TSEG2 取 2,采样点约 87.5%,这个值在总线距离较长时有更好的抗干扰能力。如果板卡晶振不是 16MHz,上述参数要按比例重新计算。

3.4 接收中断与总线错误处理

SJA1000 的 INT 引脚低电平有效,接到 ADμC812 的 INT0 或 INT1。进入中断服务程序后,必须先读中断寄存器 CAN_IR 判断事件源,再读取接收数据,最后释放接收缓冲。顺序反了会导致中断标志没清干净,程序会不停进中断。

#define CAN_RXFI 0x10 /* PeliCAN 模式下接收帧信息起始地址 */ void ext0_isr() interrupt 0 { unsigned char ir, len, i; ir = SJA_BASE[CAN_IR]; if (ir & 0x01) { /* 接收中断位 */ len = SJA_BASE[CAN_RXFI] & 0x0F; /* 低 4 位是 DLC */ for (i = 0; i < len; i++) { rx_data[i] = SJA_BASE[CAN_RXFI + 1 + i]; } rx_len = len; SJA_BASE[CAN_CMR] = 0x04; /* 释放接收缓冲 */ /* 在这里把 rx_data/rx_len 交给 DeviceNet.c 解析 */ } if (ir & 0x04) { /* 总线错误中断 */ SJA_BASE[CAN_MOD] = 0x01; /* 回到复位模式清错误 */ SJA_BASE[CAN_MOD] = 0x00; /* 再回正常模式 */ } }

这段代码里,CAN_RXFI 在正常模式与复位模式的 AMR3 共用地址,所以不能在初始化后继续用 AMR 相关逻辑访问同一位置。接收缓冲释放命令必须写在读完数据的最后一位之后,否则当前缓冲区会立即被覆盖。总线错误中断的处理方式是“复位模式清状态,再退出”,但现场要注意:BusOff 恢复不能这样直接硬来,详见最后一部分。

4. 从站状态机与网络管理:上线与超时处理

4.1 DeviceNet 从站状态机

不含主站时,从站上电后要经历初始化、等待重复 MAC ID 检测、预操作、运行等阶段。在预操作状态只能走显式报文,进入运行状态后才允许发送 I/O 报文。主站通常会在预操作状态下用 Allocate_Device 来分配连接,从站收到后用 Set_Attributes 配置参数,最后进入运行状态。工程里的 DeviceNet.c 很大一块是在做状态迁移。

下面这个表是常见的状态与事件:

状态进入条件允许的行为
等待 MAC ID上电完成发送重复 MAC ID 检测,等待 200ms
预操作重复检测无冲突响应 UCMM 显式报文
运行收到 Allocate响应显式报文和 I/O 轮询
通信故障BusOff 或看门狗超时停止 I/O,主动报告网络状态

这个状态机的核心是“不允许从预操作直接跳到故障,再自动回运行”。任何异常导致的通信故障都要回到等待 MAC ID 重新上线,否则网络中会出现一个“活着但不听主站”的节点。

4.2 重复 MAC ID 检测

每个 DeviceNet 节点在正式上线前必须广播重复 MAC ID 检测报文,确认网络里没有第二个相同 MAC ID。这个报文用的 CAN ID 是 0x3FF,数据域第一个字节高五位是源 MAC ID。如果收到回应,说明冲突,从站要亮红灯并停止上线。

void device_dup_mac_check(unsigned char mac_id) { CAN_FRAME tx; tx.id = 0x3FF; tx.len = 1; tx.data[0] = (mac_id << 3) & 0xF8; /* 高 5 位放 MAC ID */ can_send(&tx); }

这段代码的关键在于数据域构造。DeviceNet 的重复 MAC ID 检测报文数据字节高五位是 MAC ID,低三位作为命令等标志位。从站发出后要启动一个 200ms 的监听窗口,在这期间如果收到相同 MAC ID 的报告,就认为网络上已有同名节点。我一般在监听窗口里只接收这个 ID,其他报文一律丢弃,避免正常通信干扰上线流程。

4.3 定时器驱动的超时处理

ADμC812 自带三个定时器,工程里 timer.c 负责产生 1ms 或 10ms 的时基。从站的显式连接如果持续 10s 没有活动,按照 DeviceNet 规范应该释放连接;重复 MAC ID 检测也需要 200ms 定时。用定时器 0 做 1ms 时基,每次中断对多个标志计数。注意不要在中断里做协议解析,只做计数和置位,主循环里再判断。

volatile unsigned int dup_mac_count = 0; volatile unsigned int explicit_idle = 0; volatile bit dup_mac_active = 0; volatile bit explicit_conn_open = 0; void timer0_isr() interrupt 1 { TH0 = 0xFC; /* 1ms 初值,按 12MHz 晶振 */ TL0 = 0x66; if (dup_mac_active && dup_mac_count < 200) { dup_mac_count++; if (dup_mac_count >= 200) { dup_mac_active = 0; /* 200ms 监听结束 */ } } if (explicit_conn_open) { explicit_idle++; if (explicit_idle >= 10000) { explicit_conn_open = 0; /* 10s 无显式消息,连接超时 */ } } }

这段逻辑适合任何 8051 系列,不止 ADμC812。注意 TH0/TL0 的重载值取决于系统晶振,如果 ADμC812 用了 11.0592MHz 而不是 12MHz,初值要重新算,否则所有时基都会漂移。硬件调试时我通常会在主循环里翻转一个 GPIO,用示波器量周期来确认时基精度。

5. Keil C51 工程编译、烧录与现场排错

5.1 编译输出与烧录

这份工程用 Keil C51 编译,DeviceNet.Uv2 是多文件工程,源文件里有 STARTUP.A51、DeviceNet.c、SJA1000.C、timer.c、User.c。编译成功后会得到 DEVICENET_ADC.hex。ADμC812 支持串口在线烧写,把 PSEN 拉低、复位后进入串行下载模式,用 ADI 的 Flash 工具走 UART 烧录即可,不需要额外编程器。

5.2 两个高频坑:不复位和时序

第一个坑是 SJA1000 根本没进入复位模式。很多人把寄存器地址写错,尤其是 ACR/AMR 和 BTR0/BTR1 的地址在不同模式下是复用的。如果读回 CAN_MOD 的复位模式位一直是 0,说明片选地址、ALE 极性或者复位时序有问题。第二个坑是 ADμC812 外部总线访问速度不够慢。SJA1000 是较慢的外设,在 P0/P2 上读寄存器时要确保足够的等待周期。实在不行就在每次访问前加一点空指令,虽然不优雅,但能稳定。

5.3 主站扫描与 CAN 抓包验证

先设置 125kbps,用串口转 DeviceNet 主站扫描。能看到 Identity 对象的 VendorID、ProductType,说明对象字典和显式报文链路已经打通。如果扫描不到,不要急着改协议,先看 0x3FF 重复 MAC ID 检测报文有没有下来。这个报文是判断 SJA1000 发送通道有没有通的关键。抓不到就是硬件或驱动层问题,抓到了但主站不回应,才是协议层问题。

工程里还有一个值得借鉴的细节:所有报文长度栏都用 DLC,不要用 sizeof(struct),CAN 帧数据的字节序与 C 结构体可能不一致,尤其在连接分配后的响应报文里。这一点会在 DeviceNet 主站返回“请求无法解析”时浪费你大量时间。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询