当前位置: 首页 > news >正文

嵌入式协议转换器设计:CAN总线与UART串口的双向透明通信实现

1. 项目概述:一个经典的嵌入式协议转换器设计

最近在整理老项目资料,翻出来一个十几年前做的CAN总线转串口(UART)的协议转换器程序。这个项目在当时是用于一个汽车电子测试台架的,核心任务是把上位机(PC)通过串口发送的调试指令,透明地转发到CAN总线上,同时把CAN总线上的节点响应数据抓取回来,通过串口打印给上位机。说白了,就是一个双向的、透明的“翻译官”。虽然用的是老掉牙的P89C61X2和SJA1000,但整个设计思路——中断与查询的配合、帧边界判定、看门狗守护——在今天看来依然非常经典和实用,尤其适合刚接触嵌入式通信或者汽车电子的朋友理解底层通信的“脉搏”。如果你正在为如何让MCU在CAN和UART之间可靠地搬运数据而头疼,或者想了解裸机环境下如何优雅地处理多路数据流,那这个“古董级”的代码里藏着不少值得咀嚼的干货。

2. 核心设计思路与架构解析

2.1 需求定义与方案选型

这个项目的核心需求很明确:实现CAN总线与UART串口之间的双向、透明、无损的数据转发。“透明”意味着协议转换器不修改应用层数据内容,只负责物理层和数据链路层的协议转换。这常用于调试、网关、数据记录等场景。

当时选型基于几个现实考量:

  1. 成本与资源:项目对成本敏感,主控选择了Philips(现NXP)的P89C61X2,这是一颗经典的8051内核MCU,内置256字节RAM和64KB Flash,资源对于这个任务绰绰有余。
  2. 实时性与可靠性:汽车电子环境恶劣,通信必须可靠。因此,CAN控制器选择了NXP的SJA1000,独立于MCU,稳定性好,且支持强大的错误管理和帧过滤功能。
  3. 接口匹配:上位机调试工具普遍使用串口(RS-232),因此UART是必然选择。MCU自带的UART外设正好满足需求。
  4. “透明传输”的实现难点:难点不在于单字节收发,而在于如何正确识别一帧数据的开始与结束。UART是流式字节,没有内置的帧结构;而CAN是天然带帧标识(ID和数据长度码DLC)的报文。如何把一串连续的UART字节流,准确地切割成一个个CAN报文发出去,是设计的核心。

基于以上,确定了**“UART接收中断+定时器超时判定帧结束,CAN发送查询;CAN接收中断,UART发送查询”**的混合架构。中断保证响应及时性,查询简化了流程控制,混合使用能在有限的资源下达到较好的性能平衡。

2.2 系统整体工作流程

整个系统像一个高效的双向搬运工,其核心状态机可以这样理解:

  1. 上电初始化:配置MCU的IO、时钟、看门狗,初始化SJA1000的CAN控制器(设置波特率、验收滤波、工作模式),初始化UART(设置波特率、中断)。
  2. UART到CAN路径(下行)
    • 字节接收:UART每收到一个字节,产生中断,将字节存入接收缓冲区UART_RX_Data,并重置一个定时器
    • 帧结束判定:如果在这个字节之后,超过一定时间(如5个字节的传输时间)没有收到下一个字节,定时器就会溢出中断。中断服务程序认为一帧UART数据已经接收完毕,随后将缓冲区内的数据打包,启动CAN发送流程。
    • CAN报文组装与发送:根据配置(标准帧/扩展帧),将UART接收到的数据按每8字节一组(或不足8字节)封装成CAN数据帧,通过查询SJA1000发送缓冲区状态的方式,将报文发送到CAN总线上。
  3. CAN到UART路径(上行)
    • 报文接收:SJA1000收到一个完整的CAN帧后,通过外部中断0通知MCU。
    • 数据提取:CAN中断服务程序从SJA1000的接收缓冲区中,读出帧ID和数据载荷,存入CAN_RX_Data缓冲区。
    • 数据转发:随后,程序通过查询方式,将CAN_RX_Data缓冲区中的数据,一个字节一个字节地通过UART发送给上位机。
  4. 守护与恢复:独立看门狗定时器(由MAX1232芯片实现)在整个main循环中被定期“喂狗”。如果程序跑飞或陷入死循环,超过1.2秒未喂狗,系统将硬复位,确保在恶劣环境下能自我恢复。

这个流程的关键在于定时器充当了UART字节流“帧切割器”的角色,这是实现透明协议转换的精髓。

3. 关键模块的深度剖析与实现

3.1 帧边界判定:定时器的巧妙应用

这是本项目代码中最具巧思的部分。UART本身是异步串行通信,只有起始位、数据位、停止位,没有“帧结束符”。早期有些方案用特定字符(如0x0D,0x0A)作帧尾,但这破坏了数据的“透明性”,因为帧尾字符不能出现在正常数据中。

本程序采用的是一种基于时间的帧界定法,非常经典:

  • 原理:在连续的数据流中,同一帧内的两个字节之间的间隔非常短(取决于波特率),而两帧数据之间的间隔则相对较长。我们只要找到一个时间阈值,能区分开“帧内间隔”和“帧间间隔”即可。
  • 实现:在UART_ini()中,定时器0被初始化为一个16位定时器,但并未启动。在串口中断服务程序RX_INT()中,每收到一个字节,就做两件事:1. 将字节存入缓冲区并递增索引;2.重置并重启定时器0TH0/TL0 = temp_TH0/TL0; TR0 = 1;)。
  • 阈值计算:代码注释中提到“5个字节传送时间”。以波特率9600bps为例,传输一个字节(10位,含起始停止位)需要约1.04ms。5个字节时间约为5.2ms。temp_TH0/TL0就是根据这个时间计算出的定时器初值。如果5.2ms内没有收到新字节,定时器就会溢出,触发Timer0_ISR中断。
  • 中断处理:在定时器中断里,程序认为一帧数据已经收完。它将UART_DataLength(当前帧字节数)赋给UART_Length,并置位CAN_flag,通知主循环进行CAN发送。同时清零UART_DataLength,为接收下一帧做准备。

注意:这个“5个字节时间”是一个经验值,需要根据实际应用的上位机发送习惯和波特率调整。如果上位机发送流之间有更长的延迟,这个值可以增大;如果想更快地转发,可以减小,但必须大于一帧内字节的最大可能间隔。

3.2 CAN控制器SJA1000的驱动剖析

SJA1000是当时最流行的独立CAN控制器,代码中对它的操作是标准的寄存器读写。

  • 初始化 (CAN_init):

    1. 进入复位模式:向模式寄存器(MOD_CAN1)写0x01,请求复位,并等待复位位被置起。
    2. 配置时钟分频器(CDR)0xc8选择PeliCAN模式,关闭CLKOUT输出。
    3. 设置验收滤波(ACR,AMR):代码中ACR_ID全0,AMR_ID全0xFF,意味着接收所有CAN报文(不滤波)。在实际应用中,这里需要根据目标节点的CAN ID进行设置,以减少MCU的中断负担。
    4. 设置总线定时(BTR0,BTR1):这是配置CAN波特率的关键。代码中通过查表CAN_BTR0/1[10]提供了从5K到500K共10种常用波特率的配置值。例如,CAN_BTR0[6]CAN_BTR1[6]对应100Kbps(汽车CAN最常用速率)。这些值是根据SJA1000的时序公式,结合8MHz的晶振频率计算出来的。
    5. 设置输出控制(OCR)0xaa配置为正常输出模式,推挽。
    6. 退出复位模式:清除模式寄存器的复位位,进入正常工作模式。
  • 发送数据 (CAN_Transmit): 发送采用查询方式。函数首先检查发送缓冲区状态寄存器(SR_CAN1)的“发送缓冲区空”位(0x04)。为空后,开始组装发送帧:

    1. 填写帧信息(TXFrameInfo1):最高位表示帧格式(0标准,1扩展),低4位表示数据长度DLC。代码支持标准帧和扩展帧。
    2. 填写标识符(TXID1~4):将CAN_TX_ID数组中的ID写入相应寄存器。标准帧只用前两个。
    3. 填写数据(TXDATA1~8):从CAN_TX_Data缓冲区中取出数据填入。
    4. 触发发送:向命令寄存器(CMR_CAN1)写入Request_TX(0x01),SJA1000便会自动将报文发送到总线上。 代码中还处理了数据长度不是8倍数的情况,将其分为整数个完整帧和一个剩余的不完整帧发送,保证了数据的完整性。
  • 接收数据 (CAN_Receive): 接收采用中断方式。CAN接收中断CAN_ISR被触发后,首先读取中断寄存器(IR_CAN1)判断中断源。如果是接收中断(0x01),则调用CAN_Receive函数。

    1. 读取帧信息(RXFrameInfo1):判断是标准帧还是扩展帧,并获取数据长度DLC。
    2. 读取数据:根据帧格式,从RXDATA1开始的寄存器中,读取DLC指定的字节数,存入CAN_RX_Data缓冲区。
    3. 释放缓冲区:向命令寄存器写入ReleaseRXBuf(0x04),释放接收缓冲区,以便接收下一帧。
    4. 触发UART转发:在中断服务程序中直接调用UART_Transmit(),将收到的数据通过串口发出。

3.3 串口UART的配置与数据收发

串口配置相对简单,采用模式1(8位数据,可变波特率),定时器1作波特率发生器(模式2,自动重载)。

  • 波特率计算:代码中UART_BTR数组提供了1200, 2400, 4800bps的定时器重载值。以11.0592MHz晶振为例,波特率计算公式为:波特率 = (2^SMOD / 32) * (Fosc / (12 * (256 - TH1)))。当SMOD=0TH1=0xFA(250)时,波特率为(1/32)*(11059200/(12*(256-250))) = 9600?这里代码注释与数组值似乎对不上,实际计算0xFA对应的是9600bps(SMOD=0)。这可能是个笔误,实际应用时需要根据公式重新计算并验证。
  • 发送:采用查询等待方式UART_Send_Byte,简单可靠。
  • 接收:如前所述,采用中断方式,核心任务是收字节和“喂”定时器。

3.4 看门狗与系统健壮性设计

汽车电子对稳定性要求极高。代码中使用了外部看门狗芯片MAX1232,其溢出时间约为1.2秒。

  • “喂狗”操作 (W_WDT):通过翻转一个GPIO(P3^4)引脚产生一个负脉冲。这个操作被嵌入到main函数的死循环中,以及Delay函数和CAN_Transmit等可能耗时较长的函数里。
  • 作用:如果程序因为干扰跑飞,无法正常执行循环,超过1.2秒没有产生喂狗脉冲,MAX1232就会触发复位信号,让MCU重启,使系统从故障中恢复。这是一种重要的硬件容错机制。

4. 代码实操要点与移植指南

4.1 硬件连接要点

要让这段代码跑起来,硬件连接必须正确:

  1. MCU与SJA1000
    • 数据/地址总线:P89C61X2的P0口(地址数据复用)经锁存器(如74HC373)后,低8位地址(A0-A7)与数据(D0-D7)连接到SJA1000的对应引脚。
    • 片选:SJA1000的/CS引脚连接至P2.7,因此其基地址为0x7F00(当P2.7=0时选中)。代码中的CS1_SJA1000宏正源于此。
    • 读写控制:MCU的/RD/WR连接SJA1000的/RD/WR
    • 中断:SJA1000的/INT引脚连接MCU的/INT0(P3.2),下降沿触发。
    • 复位:SJA1000的/RST最好与MCU复位信号联动,或由MCU一个GPIO控制。
    • 晶振:SJA1000的XTAL1/2接8MHz晶振。
  2. MCU与MAX1232:看门狗芯片的WDI引脚接MCU的P3.4,/RESET引脚接MCU的/RST引脚。
  3. 电平转换:MCU的UART(TTL电平)需通过MAX232等芯片转换为RS-232电平,才能连接PC串口。CAN总线侧,SJA1000的TX0/RX0需通过PCA82C250或TJA1050等CAN收发器连接到物理CAN总线上。

4.2 关键参数配置与计算

  1. 系统时钟与定时:确认MCU晶振为11.0592MHz。这个频率特别适合产生标准的串口波特率。定时器0的初值temp_TH0/TL0需要根据你期望的“帧间超时时间”重新计算。公式为:定时时间 = (65536 - TH0TL0初值) * (12 / Fosc)。例如,要实现5ms超时,初值 = 65536 - 5000*11059200/12 ≈ 65536 - 4608 = 60928 (0xEE00),则TH0=0xEE,TL0=0x00
  2. CAN波特率配置:代码中的CAN_BTR0/1查表是基于SJA1000的8MHz时钟。如果你用的晶振频率不同,必须重新计算这些值!计算公式涉及波特率预分频器(BRP)、同步跳转宽度(SJW)、时间段1(Tseg1)和时间段2(Tseg2)。建议使用NXP官方提供的配置工具(如SJA1000波特率计算器)来生成正确的BTR0BTR1值。
  3. UART波特率配置:确认UART_BTR数组中的值与你期望的波特率匹配。使用11.0592MHz晶振,常用波特率对应的TH1值(SMOD=0)为:9600->0xFD, 4800->0xFA, 2400->0xF4。
  4. 验收滤波设置:当前代码设置为接收所有报文(AMR全0xFF)。在实际网络中,为了减轻MCU负担,应设置具体的ACRAMR值。例如,只接收ID为0x100的标准帧:ACR0=0x00, ACR1=0x01, AMR0=0x00, AMR1=0x00(需根据SJA1000滤波规则仔细设置)。

4.3 移植到现代MCU的思考

虽然基于8051,但其设计思想完全适用于STM32、GD32、ESP32等现代MCU。

  1. 外设驱动替换:将直接操作SJA1000寄存器的代码,替换为使用MCU内置CAN控制器的HAL库或LL库函数。发送、接收、配置滤波器的API不同,但逻辑一致。
  2. 中断管理:现代MCU的中断系统更强大,可以将UART接收中断、CAN接收中断、定时器中断更清晰地组织,优先级设置也更灵活。
  3. 缓冲区管理:原代码使用全局数组作为缓冲区,在高速或大数据量场景可能不够用。可以升级为环形缓冲区(FIFO),提高数据吞吐效率和安全性。
  4. 超时判定:除了定时器,还可以利用现代MCU的UART空闲中断(IDLE)来检测帧结束,更加精准和高效。
  5. 代码结构:可以将CAN驱动、UART驱动、协议转换逻辑分层,提高代码的可读性和可移植性。

5. 常见问题排查与调试心得

在实际调试这个转换器或类似项目时,以下几个坑我几乎每次都遇到:

5.1 通信不通的排查步骤

  1. 查电源与时钟:最基础也最易忽略。用示波器测量MCU和SJA1000的晶振引脚,确认起振且频率正确。测量电源电压是否稳定。
  2. 查物理连接
    • CAN侧:用示波器测CAN_H和CAN_L之间的差分信号。上电后,总线应有约2.5V的隐性电平。发送数据时,应看到明显的差分脉冲。确保终端电阻(通常120Ω)已正确连接在总线两端。
    • UART侧:用示波器测MCU的TXD引脚,发送数据时应有高低电平变化。确认电平转换芯片(如MAX232)工作正常。
  3. 查软件配置
    • 波特率CAN和UART的波特率不匹配是头号杀手。务必确认转换器、上位机、CAN总线其他节点的波特率设置完全一致。特别是CAN波特率,计算复杂,建议先用官方工具算出寄存器值。
    • SJA1000模式:确认MOD寄存器已正确退出复位模式。可以通过读取MOD寄存器来验证。
    • 中断:确认MCU的全局中断EA以及相应外设中断(EX0, ES)已使能。可以在中断服务程序里翻转一个LED灯来测试中断是否进入。
  4. 逻辑分析仪是神器:如果条件允许,用逻辑分析仪同时抓取MCU的UART_TX、UART_RX、CAN_TX、CAN_RX(指SJA1000的TX0/RX0引脚)以及/INT引脚。可以清晰地看到数据流向、时序关系、中断触发情况,绝大部分通信问题都能一目了然。

5.2 数据错误或丢失的可能原因

  1. 缓冲区溢出:原代码的UART_RX_Data缓冲区大小为255,如果单帧UART数据超过255字节,会导致数据丢失。务必根据应用场景评估缓冲区大小。
  2. 定时器超时时间设置不当:如果UART帧内字节间隔偶尔大于你设置的超时时间(比如因为上位机软件或操作系统调度),会导致一帧数据被错误地切割成多帧发送。适当增大超时时间,或改用更可靠的帧结束判定方法(如特定结束符,如果协议允许)。
  3. CAN发送未检查错误:代码中CAN_Transmit函数只检查了发送缓冲区是否空(SR_CAN1 & 0x04),但没有检查总线关闭、错误警告等状态。在CAN_ISR中,对于非接收中断,只是简单置位了CAN_ERROR_flag然后重新初始化。在实际应用中,应该更细致地处理错误,比如读取错误计数器(ECC,RXERR,TXERR),并尝试恢复。
  4. 中断服务程序过长CAN_ISR中断服务程序中调用了UART_Transmit,而UART发送是查询等待的。如果CAN报文很长或波特率很低,会导致中断服务时间过长,可能影响其他中断(如UART接收)的响应。可以考虑在中断中只置标志位,将UART发送放到主循环中执行。

5.3 稳定性提升建议

  1. 增加软件看门狗:除了硬件看门狗,可以在主循环的关键节点设置“软件看门狗”标志,由一个定时器中断定期检查。如果某个任务长时间阻塞,可以触发复位或错误处理。
  2. 数据校验:虽然要求“透明传输”,但可以在应用层增加简单的校验和(如累加和、CRC8)随数据帧一起发送。接收方验证校验和,错误则丢弃或请求重发,提升数据可靠性。
  3. 状态指示:充分利用LED灯。例如,LED1闪烁表示CAN发送活动,LED2闪烁表示CAN接收活动,LED3常亮表示UART通信正常,LED4快速闪烁表示看门狗即将复位等。这对现场调试非常有帮助。
  4. 版本与配置信息:在代码中固化一个版本字符串和关键配置(如波特率),并通过一个特定的CAN ID或串口命令查询返回,便于现场维护和诊断。

回过头看,这个项目代码风格质朴,没有花哨的框架,但每一处设计都直指嵌入式通信的核心问题:实时性、可靠性、资源约束。它像一本生动的教科书,展示了在资源有限的单片机上,如何通过巧妙的中断与定时器配合,完成异步流到标准报文的协议转换。即使今天有了更强大的芯片和更便捷的库函数,理解这种底层的、直接操作寄存器的编程思想,对于解决那些最棘手的硬件调试问题,依然有着不可替代的价值。在调试类似问题束手无策时,不妨回归最本质的信号流与状态机,用示波器和逻辑分析仪一步步追踪,往往能拨云见日。

http://www.jsqmd.com/news/957773/

相关文章:

  • 2026年 国际物流专线推荐榜单:深圳/中美/中欧/中英/中日/东南亚专线实力派公司精选 - 品牌企业推荐师(官方)
  • SUMO进阶:利用TraCI Python接口实现车辆轨迹实时监控与数据提取
  • 会议视频快速转文字提取音频,实用办公工具实测 - 品牌测评鉴赏家
  • 同样是数字工厂,为什么别人降本千万,你却越上越亏?
  • 2026年10款降AIGC软件亲测:最高AI率100%直降至0.12% - 降AI小能手
  • Veo风格迁移部署踩坑清单:从A100到RTX 4090,6类硬件下显存溢出的5种精准定位法(含nvidia-smi实时诊断脚本)
  • 1986-2015年全球30米分辨率城镇用地扩张占用水体时空数据集
  • 新手福音:用快马AI生成带注释的comfyuiv8组件学习项目
  • HiBit Uninstaller:彻底卸载流氓软件的终极神器(附Hibit uninstaller官网安装包)
  • GHelper终极指南:华硕笔记本性能管家完全使用教程
  • 深圳本地五大搬家公司精选:2026最新实测红榜,实力靠谱商家一览 - 从来都是英雄出少年
  • 缺失值处理实战:从类型识别到下游模型敏感性测试
  • 出差连赶三场客户对接会攒了6小时录音 试了多款会议纪要模板后2026我挖到高效整理的靠谱方
  • Flutter | 商城项目鸿蒙(OpenHarmony)适配实战
  • 【荔湾区】骑楼趟栊间的焕然如新——2026荔湾单位保洁开荒三强纪事 - 广州搬家老班长
  • 以AI治理AI!悬镜原创“AI智能体疫苗技术”硬核守护智能体运行时安全
  • Hermes Verification协议:从代码到证据的闭环验证
  • Shiply App热修复紧急发布流程
  • 什么证件照制作工具好用?2026最全证件照工具实测对比推荐 - 科技大爆炸
  • PyAutoGUI进阶玩法:结合Pillow实现游戏自动刷图与软件自动化测试实战
  • 调参不再玄学:手把手教你用吴恩达的‘试错循环’优化你的第一个深层神经网络
  • 终极TikTokenizer指南:如何精准计算AI提示词成本并节省80%费用
  • 独立思考真正的意义:拥有自己的大脑
  • 2026实测:专业降AIGC工具选这款就对了3秒改写无痕迹 - 降AI小能手
  • 2026国际EMBA世界排名榜单解析|顶尖国际化EMBA项目优势对比
  • VoidZero 加入 Cloudflare,Vite 发展获更多资源且核心特质不变
  • Arduino ESP32:从物联网新手到专业开发者的终极指南
  • 轻量级本地图书管理工具:Python+PyQt5+SQLite一键运行
  • 从502错误到丝滑pub get:一份Flutter镜像配置的防坑与自动化配置指南
  • 2026这6款硬核降AIGC平台大起底,一键让AIGC率直逼绝对安全线! - 降AI小能手