STM32 CAN总线工程实践:从协议原理到USB-CAN转换器实现
1. CAN总线协议专题教程:工程实践路径与知识体系构建
嵌入式系统中,现场总线技术是工业控制、汽车电子和智能设备通信的基石。在众多总线协议中,CAN(Controller Area Network)因其高可靠性、强抗干扰能力、非破坏性仲裁机制和完善的错误检测与处理机制,成为实时性要求严苛场景下的首选。本系列教程聚焦于STM32平台下CAN总线的工程化实现,不以概念堆砌为目标,而以“可运行、可调试、可扩展”的真实项目为牵引,构建从协议原理到产品级应用的完整技术闭环。
本教程并非孤立存在,其知识结构建立在扎实的底层外设能力之上。在进入CAN专题前,已系统完成涵盖95%常用外设的238小节基础篇教学。这并非简单的功能罗列,而是围绕STM32 HAL库的工程化使用范式展开:从GPIO的推挽/开漏输出模式选择依据,到USART异步通信中波特率误差与采样点位置的权衡;从TIM定时器输入捕获的边沿触发同步机制,到ADC多通道规则序列扫描与DMA搬运的时序配合;从I2C总线SCL时钟拉伸对主从机状态机的影响,到SPI全双工模式下NSS信号电平与时序的精确控制。这些基础模块的深度掌握,是理解CAN控制器寄存器配置逻辑的前提——因为CAN的位定时、同步段、传播段、相位缓冲段等参数,其本质与TIM的计数器分频、重装载值设定同属一类时序控制问题;CAN的过滤器配置,其寄存器映射方式与USART的中断使能寄存器(USART_CR1)或GPIO的输出类型寄存器(GPIO_OTYPER)具有相同的底层抽象层级。
因此,本教程的知识演进路径是明确且不可逆的: 先掌握“如何让一个引脚稳定输出高低电平”,再理解“如何让两个节点通过差分信号可靠交换一帧数据”;先熟悉“中断服务函数如何被CPU识别并跳转执行”,再剖析“CAN接收中断触发后,硬件如何自动将报文存入FIFO并更新状态标志”。 这种基于硬件行为建模的学习方法,确保开发者在面对CAN通信异常时,能精准定位问题在物理层(终端电阻、线缆阻抗)、数据链路层(位定时配置错误、ID过滤误判)还是应用层(报文解析逻辑缺陷)。
2. 教程结构:从协议认知到产品落地的双轨演进
本专题严格划分为“基础篇”与“提高篇”两个逻辑清晰、目标明确的阶段,二者构成递进式的能力跃迁模型。
2.1 基础篇:构建CAN通信的最小可行系统
基础篇的核心目标是建立对CAN协议栈的“肌肉记忆”式操作能力。它不追求功能的复杂性,而强调配置的确定性与现象的可观测性。整个过程围绕一个核心实验展开: 两块STM32开发板通过CAN总线实现点对点数据交互。 所有代码均基于ST官方HAL库,确保与主流开发环境(STM32CubeIDE、Keil MDK)无缝兼容。
该阶段的教学内容组织遵循“原理-配置-验证”三段论:
- 协议原理部分 ,摒弃教科书式的OSI模型套话,直指工程痛点。例如,讲解CAN的非破坏性仲裁机制时,会同步展示当节点A发送ID=0x123、节点B发送ID=0x110时,总线上实际出现的逐位电平竞争波形,并解释为何ID数值更小的报文能赢得总线使用权——这直接关联到CAN过滤器ID掩码(CAN_FilterMaskId)的设置逻辑。
- 配置环节 ,所有参数均给出工程取值依据。以CAN_BTR(位时间寄存器)为例,不会仅列出 CAN_BTR.SJW = 1; CAN_BTR.TS1 = 13; CAN_BTR.TS2 = 2; ,而是阐明:TS1(时间段1)需覆盖信号传播延迟、采样点偏移及同步跳转宽度,13个Tq(Time Quantum)是针对PCB走线长度≤20cm、波特率500kbps的典型经验值;TS2(时间段2)必须≥2Tq以满足ISO 11898标准对采样点窗口的要求;SJW(重新同步跳转宽度)设为1Tq,在多数工业现场已足够应对晶振温漂。
- 验证手段 ,采用多维度交叉确认。除常规的串口打印收发数据外,强制要求使用逻辑分析仪捕获CAN_H/CAN_L差分信号,观察位填充、ACK应答槽、错误帧等协议细节。这种“眼见为实”的验证方式,是区分理论玩家与工程实践者的关键分水岭。
值得注意的是,本阶段明确指出一个常被初学者忽视的事实: STM32的CAN外设在HAL库中不支持DMA传输。 这并非HAL库的设计缺陷,而是由CAN协议本身的特性决定。CAN报文长度可变(0-8字节),且接收事件具有高度不确定性(可能在任意时刻突发),这与DMA预设固定地址和长度的搬运模式存在根本冲突。因此,所有数据收发均通过轮询(Polling)或中断(Interrupt)方式完成。轮询适用于低负载、确定性高的场景,其优势在于代码逻辑简单,无上下文切换开销;中断则适用于多任务环境,但必须严格管理中断服务函数(ISR)的执行时间,避免因CAN ISR过长导致其他高优先级中断被阻塞。这一限制深刻影响着后续提高篇的系统架构设计。
2.2 提高篇:CAN-USB串口转换器的产品级实现
提高篇将基础篇的单一通信能力,升维为具备完整人机交互与协议转换功能的嵌入式产品。其核心载体是一个“CAN总线转USB虚拟串口转换器”,这是一个在工业现场真实存在的标准化工具,其价值远超教学演示:工程师可将其连接至PC,通过串口调试助手实时监控CAN网络上的所有报文,进行故障诊断、协议逆向与数据采集。
该产品的功能规格定义清晰:
- 双向协议转换 :实现CAN帧与UART帧的无损映射。CAN数据帧(Data Frame)对应串口发送的ASCII十六进制字符串(如 001#1122334455667788 ),远程帧(Remote Frame)则映射为特定指令(如 R001 )。
- 动态配置能力 :所有关键参数均可通过串口AT指令在线修改,无需重新烧录固件。这包括:
- UART波特率( AT+UART=115200 )
- CAN波特率( AT+CAN=500000 )
- CAN工作模式(正常模式 AT+MODE=0 / 环回自测模式 AT+MODE=1 )
- 报文过滤规则( AT+FILTER=ID,0x100,0x7FF 配置标准帧ID范围)
- 传输格式( AT+FORMAT=0 标准传输 / AT+FORMAT=1 格式化传输,后者添加时间戳与方向标识)
- 硬件资源协同 :项目不再局限于CAN外设,而是整合了多个基础模块:
- USB CDC类设备:利用STM32内置USB PHY,通过CDC ACM(Abstract Control Model)协议模拟串口,摆脱对外部CH340等USB转串口芯片的依赖;
- 按键与LED:提供物理复位与状态指示,例如长按按键进入固件升级模式;
- Flash存储:保存用户配置参数,实现掉电不丢失;
- 多级电源管理:在无CAN活动时自动进入低功耗Stop模式,由CAN唤醒中断(CAN_IT_WKU)触发恢复。
此阶段的教学重点已从“单个外设如何配置”,转向“多外设如何协同”。例如,当USB主机向转换器发送 AT+CAN=250000 指令时,系统需经历以下原子操作序列:USB中断接收指令 → 解析AT命令 → 调用 HAL_CAN_DeInit() 关闭当前CAN外设 → 重新计算250kbps对应的BTR寄存器值 → 调用 HAL_CAN_Init() 初始化 → 将新参数写入Flash → 通过USB返回 OK 响应。这个看似简单的指令背后,是中断嵌套管理、临界区保护、外设状态机迁移等高级工程实践。
3. HAL库CAN驱动:三大核心图谱的工程化解读
HAL库的价值在于将复杂的寄存器操作封装为语义清晰的API,但其“黑盒”特性也常导致开发者知其然不知其所以然。为破除这一迷思,本教程提炼出驱动CAN外设必须掌握的三大核心图谱: 结构体成员配置图谱、API函数调用图谱、中断回调函数图谱 。这三者共同构成了HAL库CAN驱动的完整知识骨架。
3.1 结构体成员配置图谱:寄存器映射的可视化表达
HAL库中,CAN外设的初始化完全由 CAN_HandleTypeDef 结构体承载。该结构体并非随意定义,而是对STM32参考手册中CAN_MCR、CAN_BTR、CAN_FMR等寄存器的精确C语言映射。理解其成员,即是理解硬件配置的本质。
typedef struct __CAN_HandleTypeDef
{
CAN_TypeDef *Instance; // 指向CAN1或CAN2寄存器基地址(0x40006400或0x40006800)
CAN_InitTypeDef Init; // 核心初始化参数
CAN_FilterTypeDef Filter; // 过滤器配置
HAL_LockTypeDef Lock; // 互斥锁,用于多任务环境下的线程安全
__IO HAL_CAN_StateTypeDef State; // 外设当前状态机(HAL_CAN_STATE_RESET等)
__IO uint32_t ErrorCode; // 最近一次操作的错误码
} CAN_HandleTypeDef;
其中, Init 和 Filter 子结构体是配置的核心:
typedef struct
{
uint32_t Prescaler; // 波特率预分频器,决定Tq周期。公式:Tq = (Prescaler) * Tpclk1
uint32_t Mode; // 工作模式:CAN_MODE_NORMAL(正常)/ CAN_MODE_LOOPBACK(环回)/ CAN_MODE_SILENT(静默)
uint32_t SJW; // 重新同步跳转宽度,单位Tq,范围1-4
uint32_t BS1; // 时间段1(TS1),包含PROP_SEG和PHASE_SEG1,范围1-16
uint32_t BS2; // 时间段2(TS2),即PHASE_SEG2,范围1-8
uint32_t TTCM; // 时间触发通信模式使能,用于高精度时间戳
uint32_t ABOM; // 自动离线管理,硬件自动处理BUS OFF状态恢复
uint32_t AWUM; // 自动唤醒模式,从Sleep模式自动唤醒
uint32_t NART; // 禁止自动重传,发送失败后不重试
uint32_t RFLM; // 接收FIFO锁定模式,FIFO满时丢弃新报文而非覆盖旧报文
uint32_t TXFP; // 发送FIFO优先级,0=按请求顺序,1=按报文ID优先级
} CAN_InitTypeDef;
typedef struct
{
uint32_t FilterActivation; // 过滤器使能:ENABLE/DISABLE
uint32_t FilterMode; // 过滤器模式:CAN_FILTERMODE_IDMASK(ID/MASK)/ CAN_FILTERMODE_IDLIST(ID列表)
uint32_t FilterScale; // 过滤器尺度:CAN_FILTERSCALE_32BIT(32位)/ CAN_FILTERSCALE_16BIT(16位)
uint32_t FilterIdHigh; // 过滤器ID高位(16位),标准帧ID左移5位,扩展帧ID左移3位
uint32_t FilterIdLow; // 过滤器ID低位(16位)
uint32_t FilterMaskIdHigh; // 过滤器屏蔽码高位(16位)
uint32_t FilterMaskIdLow; // 过滤器屏蔽码低位(16位)
uint32_t FilterFIFOAssignment; // 分配到哪个FIFO(0或1)
uint32_t FilterBank; // 过滤器组编号(0-27),STM32F103有14组,F4系列有28组
} CAN_FilterTypeDef;
以一个典型配置为例:
hcan1.Init.Prescaler = 6; // 若APB1时钟为36MHz,则Tq = 6 * (1/36MHz) ≈ 166.7ns
hcan1.Init.Mode = CAN_MODE_NORMAL;
hcan1.Init.SJW = CAN_SJW_1TQ;
hcan1.Init.BS1 = CAN_BS1_13TQ; // TS1 = 13Tq
hcan1.Init.BS2 = CAN_BS2_2TQ; // TS2 = 2Tq
// 总位时间 = (1 + TS1 + TS2) * Tq = (1 + 13 + 2) * 166.7ns ≈ 2.67us → 波特率 ≈ 375kbps
此处BS1与BS2的取值,直接决定了采样点位置。采样点 = 1 + TS1,即在第14个Tq时刻采样。根据ISO 11898,采样点应在位时间的60%-80%区间内,14/16=87.5%略高,故在更高波特率下需调整BS1为12,使采样点落在13/15≈86.7%,仍属可接受范围。这种基于物理时序的参数推演,是结构体图谱学习的终极目标。
3.2 API函数调用图谱:从初始化到数据收发的完整流程
HAL库API并非孤立函数,而是一个具有严格调用时序的有机整体。其生命周期可分为四个阶段:
阶段一:硬件初始化与使能
// 1. 初始化CAN外设(配置时钟、GPIO、NVIC)
HAL_CAN_Init(&hcan1);
// 2. 配置并启用过滤器(至关重要!无此步,所有报文均被丢弃)
HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig);
// 3. 启动CAN外设,进入正常工作模式
HAL_CAN_Start(&hcan1);
HAL_CAN_Init() 内部执行 __HAL_RCC_CAN1_CLK_ENABLE() 使能时钟, HAL_GPIO_Init() 配置PA11/PA12为复用推挽,并调用 HAL_NVIC_SetPriority() 和 HAL_NVIC_EnableIRQ() 配置中断。若在此阶段遗漏 HAL_CAN_ConfigFilter() ,即使物理连接正确, HAL_CAN_GetRxFifoFillLevel() 也将始终返回0,这是初学者最常踩的坑。
阶段二:数据发送(轮询与中断)
// 轮询方式:阻塞等待发送完成
CAN_TxHeaderTypeDef TxHeader;
uint8_t TxData[8] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
uint32_t TxMailbox;
TxHeader.StdId = 0x123;
TxHeader.ExtId = 0x00;
TxHeader.RTR = CAN_RTR_DATA;
TxHeader.IDE = CAN_ID_STD;
TxHeader.DLC = 8;
HAL_CAN_AddTxMessage(&hcan1, &TxHeader, TxData, &TxMailbox);
// 中断方式:注册回调,由中断触发
HAL_CAN_ActivateNotification(&hcan1, CAN_IT_TX_MAILBOX_EMPTY);
// 在回调函数中处理发送完成
void HAL_CAN_TxMailbox0CompleteCallback(CAN_HandleTypeDef *hcan) { /* ... */ }
HAL_CAN_AddTxMessage() 的返回值 HAL_StatusTypeDef 是判断发送是否成功的唯一依据。若返回 HAL_ERROR ,需检查 hcan->ErrorCode ,常见原因有:发送邮箱全满( CAN_ERROR_TXFULL )、CAN外设未启动( CAN_ERROR_NOT_INITIALIZED )。
阶段三:数据接收(FIFO管理)
// 读取FIFO0中的报文
CAN_RxHeaderTypeDef RxHeader;
uint8_t RxData[8];
if (HAL_CAN_GetRxFifoFillLevel(&hcan1, CAN_RX_FIFO0) > 0) {
HAL_CAN_GetRxMessage(&hcan1, CAN_RX_FIFO0, &RxHeader, RxData);
// RxHeader.StdId, RxHeader.DLC, RxData[] 即为接收到的数据
}
FIFO的深度为3,当连续接收超过3帧且未及时读取时,新报文将覆盖最旧报文( RFLM=DISABLE 时)。因此,在中断接收模式下,必须确保 HAL_CAN_GetRxMessage() 在 HAL_CAN_RxFifo0MsgPendingCallback() 中被快速执行,否则必然丢帧。
阶段四:错误处理与状态监控
// 主循环中定期检查
HAL_CAN_GetError(&hcan1); // 返回CAN_ErrorCodeTypeDef,如CAN_ERROR_BUSOFF
HAL_CAN_GetState(&hcan1); // 返回HAL_CAN_StateTypeDef,如HAL_CAN_STATE_BUSY_RX
CAN_ERROR_BUSOFF 是CAN网络中最严重的错误,表明节点因连续发送错误帧超过128次而被强制脱离总线。此时 HAL_CAN_GetState() 将返回 HAL_CAN_STATE_ABORTED ,必须调用 HAL_CAN_Stop() 和 HAL_CAN_Start() 进行软复位,或等待硬件自动恢复(若 ABOM=ENABLE )。
3.3 中断回调函数图谱:事件驱动的中枢神经
HAL库的中断处理采用“注册-回调”模型,所有CAN事件均由统一的中断服务函数 CAN1_RX0_IRQHandler (或 CAN1_RX1_IRQHandler 、 CAN1_SCE_IRQHandler )分发。开发者只需实现对应的弱定义回调函数,HAL库自动在ISR中调用。
| 中断源 | 对应回调函数 | 触发条件 | 工程意义 |
|---|---|---|---|
| RX FIFO0 Message Pending | HAL_CAN_RxFifo0MsgPendingCallback() |
FIFO0中有新报文到达 | 最常用 ,在此函数中调用 HAL_CAN_GetRxMessage() 读取数据 |
| RX FIFO1 Message Pending | HAL_CAN_RxFifo1MsgPendingCallback() |
FIFO1中有新报文到达 | 当FIFO0满时,新报文进入FIFO1,需双FIFO管理 |
| TX Mailbox Empty | HAL_CAN_TxMailbox0CompleteCallback() |
邮箱0发送完成 | 可在此触发下一次发送,实现流控 |
| Error | HAL_CAN_ErrorCallback() |
发生错误(位错误、填充错误、CRC错误等) | 关键调试入口 ,通过 HAL_CAN_GetError() 获取具体错误码 |
一个健壮的中断处理框架示例如下:
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
static uint8_t rx_buffer[8];
CAN_RxHeaderTypeDef rx_header;
// 1. 快速读取,避免FIFO溢出
if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rx_header, rx_buffer) == HAL_OK) {
// 2. 将数据拷贝至线程安全的环形缓冲区(如CMSIS-RTOS队列)
xQueueSendFromISR(can_rx_queue, &rx_header, NULL);
xQueueSendFromISR(can_rx_data_queue, rx_buffer, NULL);
}
// 3. 清除中断标志(HAL库已自动处理,此步为冗余确认)
__HAL_CAN_CLEAR_FLAG(hcan, CAN_FLAG_RQCP0);
}
此框架将耗时的数据解析与业务逻辑(如报文转发、状态机更新)移出ISR,交由FreeRTOS任务在后台处理,确保中断响应时间最短,符合实时系统设计原则。
4. 开发板选型与生态协同:紫色开发板的工程价值
本教程所有实验均基于一款定制化的“紫色开发板”,其选型绝非随意,而是深度契合CAN教学的工程需求。该板卡的核心特征在于 硬件资源与软件生态的无缝咬合 。
4.1 硬件设计的针对性考量
- CAN物理层接口 :板载SN65HVD230高速CAN收发器,其共模电压范围(-7V至+12V)和±35kV ESD防护能力,远超通用方案,确保在工业现场强干扰环境下通信稳定。收发器的RS引脚(斜率控制)通过跳线帽可选,便于在长距离(低斜率)与高速率(高斜率)间切换。
- USB-CDC集成 :直接利用STM32F103C8T6的USB Device功能,省去外部USB转串口芯片。这不仅降低了BOM成本,更重要的是,USB与CAN共享同一颗MCU,使得两者间的零拷贝数据交换成为可能——CAN接收的数据可直接通过USB端点(Endpoint)DMA发送,极大提升吞吐效率。
- 调试与观测接口 :除标准SWD接口外,板载独立的CAN_H/CAN_L测试点,可直连示波器或逻辑分析仪;同时引出PA11/PA12的GPIO复用功能,方便在不改动硬件的前提下,将CAN引脚复用为普通GPIO进行信号注入测试。
4.2 社群与支持体系:超越硬件本身的价值
购买该开发板所获得的,远不止一块PCB。其背后是一个活跃的工程师社群,其价值体现在三个层面:
- 即时问题响应 :当在 HAL_CAN_Start() 后发现 HAL_CAN_GetState() 始终返回 HAL_CAN_STATE_BUSY 时,社群中已有数十个相似案例的排查记录——常见原因为CAN收发器的VCC未上电,或终端电阻未正确接入(总线两端各需一个120Ω电阻)。
- 固件仓库共享 :所有教程配套代码均托管于GitHub,且按功能模块化组织。例如, /examples/can_loopback/ 目录下提供了纯环回测试固件,可排除物理层故障; /examples/can_bridge/ 目录则实现了完整的CAN-USB桥接逻辑,可作为提高篇项目的起点。
- 硬件DIY指南 :对于希望深入理解的开发者,社群提供完整的原理图(PDF)与PCB布局文件(Gerber),并详细标注了每个元件的选型依据。例如,为何选用12MHz晶振而非8MHz?因为12MHz可被整除得到更精确的48MHz USB时钟(12MHz * 4 = 48MHz),避免了PLL倍频带来的时钟抖动。
这种“硬件+软件+社群”的三位一体模式,将学习成本从“独自摸索”降为“站在巨人肩膀上迭代”,是加速工程能力成长的关键基础设施。
5. 实践建议:从第一个CAN帧到稳定产品的进阶路径
基于多年一线项目经验,我为学习者梳理出一条高效、少走弯路的实践路径。这条路径的核心思想是: 每一次编译下载,都应带来一个可验证、可观测、可量化的结果。
5.1 第一步:物理层连通性验证(<1小时)
目标:在没有任何软件代码的情况下,确认CAN总线物理连接正确。
- 操作 :使用万用表测量两块开发板的CAN_H与CAN_H之间、CAN_L与CAN_L之间的电阻。正常值应为60Ω(两颗120Ω终端电阻并联)。若测得120Ω,说明只有一端接了终端电阻;若测得无穷大,说明线路断开或收发器未供电。
- 验证 :将两块板的CAN收发器VCC引脚短接到3.3V,GND相连。此时用示波器观察CAN_H/CAN_L,应能看到稳定的2.5V共模电压,差分电压摆幅约为2Vpp。这是CAN总线“呼吸”的生命体征。
5.2 第二步:环回模式下的自检(<2小时)
目标:在单块开发板上,验证CAN外设硬件与基础软件配置。
- 操作 :配置CAN工作模式为 CAN_MODE_LOOPBACK ,发送一个标准帧(ID=0x555,DLC=1,Data[0]=0xAA),并在同一块板上接收。
- 验证 :若 HAL_CAN_GetRxMessage() 成功读取到相同ID与数据,则证明:
1. MCU的CAN控制器时钟、GPIO复用配置正确;
2. HAL库初始化流程无误;
3. 编译器链接脚本未错误地将CAN相关代码优化掉。
- 关键技巧 :在 HAL_CAN_RxFifo0MsgPendingCallback() 中,立即点亮一个LED。LED闪烁即为CAN通信成功的最直观证据,比串口打印更可靠——因为串口本身也可能出错。
5.3 第三步:双板点对点通信(<1天)
目标:两块开发板通过CAN总线交换数据。
- 操作 :板A配置为发送器,周期性发送;板B配置为接收器,收到后通过串口打印。务必使用逻辑分析仪捕获CAN_H/CAN_L信号,确认总线上确实出现了预期的位流。
- 避坑指南 :若通信失败,按此顺序排查:
1. 时钟 :用示波器测量PA11/PA12的复位后状态,确认其为高阻态(非强推挽输出),否则会破坏总线;
2. 波特率 :双板必须使用完全相同的BTR寄存器值,任何一位差异都会导致同步失败;
3. 过滤器 :接收端过滤器ID与发送端ID必须匹配,且 FilterMode 与 FilterScale 配置正确。一个经典错误是:发送扩展帧(IDE=CAN_ID_EXT),但接收过滤器配置为标准帧模式(IDE=CAN_ID_STD)。
5.4 第四步:提高篇产品化演进(>3天)
当点对点通信稳定后,即可启动提高篇。此时,应将项目视为一个微型产品,引入产品开发的工程规范:
- 版本管理 :为每一个功能里程碑打Git Tag,如 v1.0-can-loopback 、 v1.1-can-usb-bridge 。
- 配置持久化 :使用 HAL_FLASHEx_DATAEEPROM_Unlock() 解锁Data EEPROM,将AT指令配置的参数(如CAN波特率)写入其中,确保掉电不丢失。
- 异常熔断 :在 HAL_CAN_ErrorCallback() 中,若连续检测到5次 CAN_ERROR_BUSOFF ,则触发看门狗复位,防止设备陷入未知僵死状态。
我在一个真实的电梯控制系统项目中,曾因忽略“过滤器配置”导致整栋楼的轿厢通信瘫痪。当时所有CAN节点均能发送,但无人能接收,排查耗时两天。最终发现是某块控制板的过滤器ID被误设为0x000,而网络中所有报文ID均大于0x100。这个教训让我坚信: 对CAN的理解,不在于能否写出初始化代码,而在于能否在总线沉默时,准确说出哪一根导线、哪一个寄存器、哪一行代码,正在扼杀通信。
更多推荐

所有评论(0)