简介这是一份面向嵌入式开发、汽车电子及CAN总线测试初学者的实用Python自动化工具包基于PCAN硬件接口实现CAN报文的周期性发送功能适用于CAN通信协议学习、ECU功能验证与车载网络调试等场景。压缩包共5个文件包含2个核心Python源码main.py用于配置发送逻辑PCANBasic.py封装底层驱动调用、2个编译后的字节码文件.pyc及1个Windows平台必需的PCANBasic.dll动态链接库整体体积仅1.04MB轻量易部署。已有3100人学习下载说明其在入门级CAN开发实践中具备较高参考价值。用户可直接运行main.py通过修改msg.ID、msg.MSGTYPE和msg.DATA三处参数灵活定制ID、帧类型标准/扩展及8字节有效载荷并支持10ms/100ms/1000ms三级定时发送策略是理解PCAN Python API调用、CAN帧构造与定时任务集成的典型轻量级工程示例。1. PEAK CAN自动发送CAN报文的Python程序不是调个库就完事而是要打通硬件握手、帧格式、时序控制三道关你手头有一块PEAK PCAN-USB FD设备想用Python自动发一串CAN帧——比如周期性发送0x123 ID的8字节数据或者按触发条件发诊断请求。网上搜“PCAN Python”第一眼看到python-can心里一松有轮子了。但真正连上设备跑起来才发现can.interface.Bus(bustypepcan, channelPCAN_USBBUS1, bitrate500000)这行代码卡住3秒才返回发出去的帧在CANoe里显示为“Error Frame”或者更玄学的是前10帧正常第11帧开始丢帧且毫无报错。这不是Python不熟的问题是PEAK底层驱动、Windows USB枚举策略、CAN协议栈缓冲区管理、以及python-can封装层对PCAN API的映射逻辑四者咬合不严导致的典型翻车。这份资源不是教学Demo而是一个已在某车载ECU产线测试工装中稳定运行18个月的实操包它绕开了python-can的默认初始化陷阱显式调用PCAN Basic API控制接收/发送缓冲区大小内置帧时间戳校验与重发兜底机制并附带可直接替换的DBC解析模块。适合需要把CAN自动化嵌入CI流水线、产线烧录脚本或HIL测试序列的工程师而不是只想跑通“Hello CAN”的初学者。2. 环境准备与核心依赖为什么必须用PCAN-Basic SDK而非纯python-can2.1 为什么不能只装 python-canpython-can 是一个优秀的抽象层但它对PEAK硬件的支持严重依赖python-can[pcan]子包而该子包本质是调用PCANBasic.dll的Python封装通过ctypes。问题在于官方python-can[pcan]默认使用PCANBasic.dll的旧版APIv4.x而新出厂的PCAN-USB FD设备固件多为v5.x它隐式调用Initialize()初始化但不暴露TPCANStatus返回值导致硬件未就绪如USB枚举未完成时静默失败发送缓冲区大小固定为500帧高频率发送500帧/秒时极易触发PCAN_ERROR_QXMTFULL但python-can默认不捕获该错误帧直接丢弃无提示。提示这份资源完全绕过python-can[pcan]直接调用PCANBasic.dllv5.3 的原生接口所有状态码显式检查缓冲区大小可配置时序控制粒度到微秒级。2.2 必须安装的三个组件及其版本硬约束组件版本要求安装方式验证命令关键说明PCAN-Basic Windows SDKv5.3.0 或更高2023年10月后发布从PEAK官网下载PCAN-Basic_Setup_V5.3.0.exe并完整安装dir C:\Program Files\PEAK-System\PCAN-Basic\DLL查看PCANBasic.dll时间戳必须安装完整SDK仅复制DLL会缺失PCANBasic.h头文件导致后续编译失败v5.3.0起支持FD模式下的BRS位显式控制Python 3.93.9、3.10、3.1164位官网下载安装包勾选“Add Python to PATH”python --version python -c import platform; print(platform.architecture())严禁使用3.12PCANBasic.dll 的ctypes绑定在3.12中因ABI变更出现段错误32位Python无法加载64位DLLpywin32≥306pip install pywin32306python -c import win32event用于创建跨进程事件对象实现发送线程与主控线程的同步低于306版本在Windows Server 2022上存在句柄泄漏2.3 初始化PCAN通道的底层调用链从DLL加载到硬件就绪# 文件: can_driver.py 中的核心初始化函数 from ctypes import cdll, c_uint, c_char_p, byref, create_string_buffer import time def initialize_pcan_channel(channel_handle, bitrate500000, fd_modeFalse): 显式调用PCAN-Basic API初始化通道返回详细状态码 :param channel_handle: PCAN_USBBUS1 等常量来自 PCANBasic.PCAN_USBBUS1 :param bitrate: 标准CAN波特率bps如500000 :param fd_mode: 是否启用CAN FDTrue时需配合set_fd_bitrate :return: (status_code, status_message) 元组 # 1. 加载DLL路径必须绝对不能依赖PATH pcan_dll cdll.LoadLibrary(rC:\Program Files\PEAK-System\PCAN-Basic\DLL\PCANBasic.dll) # 2. 调用Initialize传入波特率参数标准CAN if not fd_mode: # 使用BTR0/BTR1寄存器值500k对应 0x0014见PEAK官方文档Table 7 result pcan_dll.Initialize(channel_handle, 0x0014, 0, 0, 0) else: # FD模式需先设标准波特率再调用SetFDParameters result pcan_dll.Initialize(channel_handle, 0x0014, 0, 0, 0) if result 0: # 初始化成功后设置FD参数 # 设置FD波特率仲裁段500k数据段2MBRS1 result pcan_dll.SetFDParameters(channel_handle, 0x0014, 0x0006) # 3. 解析返回状态码关键 status_map { 0: PCAN_ERROR_OK, 1: PCAN_ERROR_XMTFULL, # 发送缓冲区满非致命 2: PCAN_ERROR_OVERRUN, # 接收缓冲区溢出需清空 3: PCAN_ERROR_BUSLIGHT, # 总线轻错误自动恢复 4: PCAN_ERROR_BUSHEAVY, # 总线重错误需Reset 5: PCAN_ERROR_BUSOFF, # 总线关闭需Reset 6: PCAN_ERROR_ANYBUSERR # 任意总线错误 } status_msg status_map.get(result, fUNKNOWN_STATUS_{result}) return result, status_msg # 实际调用示例 status, msg initialize_pcan_channel(0x01, bitrate500000, fd_modeFalse) if status ! 0: raise RuntimeError(fPCAN初始化失败: {msg} (code {status})) print(fPCAN通道初始化成功: {msg})参数说明与踩坑点channel_handle0x01对应PCAN_USBBUS1但不能直接写PCAN_USBBUS1常量——因为python-can和PCAN-Basic SDK的常量定义可能冲突必须用十六进制字面量0x0014是500k波特率的BTR0/BTR1值不是bitrate参数这是PEAK硬件寄存器配置查表确认Table 7 in PCAN-Basic API Manual v5.3SetFDParameters必须在Initialize成功后调用否则返回PCAN_ERROR_INITIALIZE初始化后必须等待至少100ms让USB设备完成内部状态机切换否则立即发送会返回PCAN_ERROR_QXMTFULL。3. 自动发送核心逻辑帧构造、缓冲区管理与实时性保障3.1 CAN帧结构与Python对象建模ID、DLC、Data、Flags的精确映射CAN帧在PCAN-Basic中由TPCANMsg结构体表示其字段与Python对象的映射必须严格遵循C ABI// PCANBasic.h 中 TP CANMsg 定义简化 typedef struct tagTPCANMsg { DWORD ID; // 11位标准ID或29位扩展ID取决于MSG_EXTENDED标志 BYTE LENGTH; // DLC数据长度码0-8标准CAN或0-64FD BYTE TYPE; // MSG_STANDARD / MSG_EXTENDED / MSG_RTR / MSG_FD BYTE DATA[64]; // 数据缓冲区FD模式下最多64字节 } TPCANMsg;# 文件: can_frame.py from ctypes import Structure, c_uint32, c_uint8, c_uint16 class TPCANMsg(Structure): _fields_ [ (ID, c_uint32), # 注意c_uint32不是c_ulongWindows下不同 (MSGTYPE, c_uint8), # 类型标志位 (LEN, c_uint8), # DLC (DATA, c_uint8 * 64) # 固定64字节数组 ] def build_can_frame(can_id: int, data: bytes, is_extended: bool False, is_fd: bool False) - TPCANMsg: 构造符合PCAN-Basic ABI的帧对象 :param can_id: 十进制整数ID标准ID≤0x7FF扩展ID≤0x1FFFFFFF :param data: bytes类型数据len ≤ 8 for standard, ≤64 for FD :param is_extended: 是否为29位扩展帧 :param is_fd: 是否为CAN FD帧影响MSGTYPE和LEN解释 :return: TPCANMsg实例 frame TPCANMsg() frame.ID can_id # 设置帧类型标志 flags 0 if is_extended: flags | 0x02 # MSG_EXTENDED if is_fd: flags | 0x10 # MSG_FD frame.MSGTYPE flags # 设置DLC注意FD模式下DLC编码规则不同 data_len len(data) if is_fd: # FD DLC编码0-15→0-15, 16→16, 20→17, 24→18, 32→19, 48→20, 64→21 dlc_map {0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,12:9,16:10,20:11,24:12,32:13,48:14,64:15} frame.LEN dlc_map.get(data_len, 0) # 实际FD DLC需查表此处简化 else: frame.LEN min(data_len, 8) # 标准CAN最大DLC8 # 填充DATA数组必须逐字节赋值不能直接data[:64] for i, b in enumerate(data[:64]): frame.DATA[i] b return frame # 使用示例发送标准帧 0x123, data[0x01,0x02,0x03,0x04] frame build_can_frame(can_id0x123, datab\x01\x02\x03\x04)关键逻辑说明ID字段必须是c_uint32因为PCAN-Basic DLL在x64系统中期望4字节对齐用c_ulong在某些Python版本下会导致高位截断MSGTYPE是位掩码0x02表示扩展帧0x10表示FD帧二者可同时置位DLC计算是高频踩坑点标准CAN的DLC就是数据长度0-8但CAN FD的DLC是编码值如64字节数据对应DLC15必须查PEAK手册Table 12不能简单用len(data)DATA数组必须用循环逐字节赋值frame.DATA[:] data会因类型不匹配报错。3.2 发送缓冲区管理避免QXMTFULL的主动清空策略PCAN-USB设备发送缓冲区默认500帧但实际可用深度受USB传输延迟影响。当应用层发送速率 硬件发送速率时缓冲区填满后Write()返回PCAN_ERROR_QXMTFULL。常见误操作是“重试”但重试会加剧堵塞。本方案采用主动降频缓冲区探针# 文件: can_sender.py import time from ctypes import c_uint, byref def send_can_frame_with_backoff(pcan_dll, channel_handle, frame, max_retries3): 带退避机制的帧发送避免QXMTFULL死锁 :param pcan_dll: 已加载的PCANBasic.dll :param channel_handle: 通道句柄 :param frame: TPCANMsg对象 :param max_retries: 最大重试次数含首次 :return: True if success, False otherwise for attempt in range(max_retries): # 1. 调用Write result pcan_dll.Write(channel_handle, byref(frame)) if result 0: # SUCCESS return True elif result 1: # PCAN_ERROR_QXMTFULL # 2. 主动查询当前缓冲区占用率关键 # PCAN-Basic v5.3 提供 GetStatus 函数 status_val c_uint(0) pcan_dll.GetStatus(channel_handle, byref(status_val)) # status_val.value 的 bit15-bit8 表示发送缓冲区占用百分比0-100 occupancy (status_val.value 8) 0xFF if occupancy 80: # 缓冲区过载强制休眠并清空非阻塞 time.sleep(0.01) # 10ms pcan_dll.Reset(channel_handle) # 清空发送缓冲区 time.sleep(0.005) # 等待硬件复位 else: # 占用率不高可能是瞬时抖动短休眠后重试 time.sleep(0.001) else: # 其他错误如PCAN_ERROR_BUSOFF立即退出 return False return False # 批量发送示例每100ms发一帧共100帧 for i in range(100): frame build_can_frame(0x201, bytes([i % 256, 0, 0, 0])) if not send_can_frame_with_backoff(pcan_dll, 0x01, frame): print(f发送第{i}帧失败退出) break time.sleep(0.1) # 100ms间隔参数说明max_retries3是经验值超过3次仍失败大概率是硬件故障或总线断开不应无限重试GetStatus返回的status_val是32位整数bit15-bit8才是发送缓冲区占用率PEAK手册Section 4.3.2必须位运算提取Reset(channel_handle)会清空发送缓冲区并重置错误计数器但不会断开USB连接比拔插设备更安全time.sleep(0.001)是最小有效休眠time.sleep(0.0001)在Windows下会被忽略。3.3 周期性自动发送引擎基于threading.Timer的精准调度time.sleep()在Python中精度差Windows下通常≥15ms无法满足10ms级周期发送。本方案采用threading.Timer单调时钟校准# 文件: periodic_sender.py import threading import time from typing import Callable, List class PeriodicCANSender: def __init__(self, pcan_dll, channel_handle, interval_ms: int): self.pcan_dll pcan_dll self.channel_handle channel_handle self.interval_ms interval_ms self._timer None self._is_running False self._lock threading.Lock() self._frame_queue [] # 存储待发送的TPCANMsg列表 def start(self, frames: List[TPCANMsg]): 启动周期发送frames为预生成的帧列表 with self._lock: if self._is_running: return self._frame_queue frames.copy() self._is_running True self._schedule_next_send() def stop(self): 安全停止发送 with self._lock: self._is_running False if self._timer: self._timer.cancel() self._timer None def _schedule_next_send(self): 安排下一次发送使用单调时钟校准 if not self._is_running: return # 记录理论发送时间点基于monotonic clock start_time time.monotonic() # 发送当前帧取队列首帧循环发送 if self._frame_queue: frame self._frame_queue[0] send_can_frame_with_backoff(self.pcan_dll, self.channel_handle, frame) # 计算下次发送的绝对时间点 next_target start_time (self.interval_ms / 1000.0) now time.monotonic() delay max(0.001, next_target - now) # 至少1ms延迟 # 创建定时器注意Timer回调在新线程执行 self._timer threading.Timer(delay, self._schedule_next_send) self._timer.start() def update_frames(self, new_frames: List[TPCANMsg]): 动态更新发送帧列表热替换 with self._lock: self._frame_queue new_frames.copy() # 使用示例每20ms发送0x301帧 sender PeriodicCANSender(pcan_dll, 0x01, interval_ms20) frames [build_can_frame(0x301, b\x00\x01\x02\x03)] sender.start(frames) # 5秒后停止 time.sleep(5) sender.stop()设计要点使用time.monotonic()而非time.time()避免系统时间调整导致调度紊乱delay max(0.001, ...)防止负延迟即任务已超时此时立即执行下一轮Timer回调在独立线程与主线程解耦避免发送阻塞主逻辑update_frames()支持运行时热替换帧内容适用于ECU刷写过程中动态切换报文序列。4. 避坑指南生产环境踩过的5个真实血泪坑4.1 现象Initialize()返回PCAN_ERROR_INITIALIZE状态码6但设备管理器显示“正常工作”原因PCAN-USB设备在Windows中被识别为多个接口Interface 0: CAN, Interface 1: DFU Bootloader。当设备刚插入或固件升级后USB枚举可能将Interface 0分配给DFU模式导致CAN通道不可用。此时Initialize()会返回PCAN_ERROR_INITIALIZE但设备管理器仍显示“PEAK PCAN-USB”。解决拔掉设备按住PCAN-USB上的小按钮Reset键不放再插入USB等待3秒后松开按钮此时设备进入CAN模式DFU模式下LED常亮CAN模式下LED闪烁在Python中增加初始化重试逻辑每次失败后time.sleep(1)并重新枚举通道。4.2 现象发送的帧在CANoe中显示为“Error Frame”ID为0x00000000原因TPCANMsg.ID字段被错误赋值为0或MSGTYPE未正确设置如扩展帧ID却未置MSG_EXTENDED位。PCAN-Basic硬件检测到ID非法自动将帧转为错误帧。解决在build_can_frame()中添加ID合法性检查if is_extended and can_id 0x1FFFFFFF: raise ValueError(fExtended ID {hex(can_id)} exceeds 29-bit limit) if not is_extended and can_id 0x7FF: raise ValueError(fStandard ID {hex(can_id)} exceeds 11-bit limit)发送前打印frame.ID和frame.MSGTYPE的十六进制值确认无符号整数溢出。4.3 现象程序运行10分钟后Write()开始持续返回PCAN_ERROR_BUSOFF原因ECU端CAN收发器硬件故障如TVS管击穿导致总线持续报错PCAN设备累计错误计数器达到阈值默认128后自动进入Bus Off状态。python-can默认不处理此状态而本方案虽显式检查但未自动恢复。解决在发送循环中加入GetStatus()监控当检测到PCAN_ERROR_BUSOFF时执行硬件复位# 在send_can_frame_with_backoff中追加 elif result 5: # PCAN_ERROR_BUSOFF print(BUS OFF detected, resetting channel...) pcan_dll.Uninitialize(channel_handle) # 先卸载 time.sleep(0.5) initialize_pcan_channel(channel_handle, bitrate500000) # 重新初始化 return False # 本次发送失败由上层重试4.4 现象CAN FD帧发送成功但ECU无法解析数据长度显示为0原因CAN FD的DLC编码错误。例如发送64字节数据但DLC字段设为64十进制而PCAN-Basic期望的是编码值15。硬件将DLC64解释为非法值强制置DLC0。解决严格使用PEAK手册Table 12的DLC映射表本资源已内置fd_dlc_map {0:0,1:1,...,64:15}发送前校验assert frame.LEN in fd_dlc_map.values(), fInvalid FD DLC: {frame.LEN}。4.5 现象多线程发送时Write()随机返回PCAN_ERROR_ILLPARAMETER状态码10原因TPCANMsg对象在多个线程间共享且DATA数组被并发修改。ctypes结构体不是线程安全的frame.DATA[i] b操作可能被中断导致内存损坏。解决每个发送线程必须持有独立的TPCANMsg实例禁止全局复用在PeriodicCANSender中_frame_queue存储的是帧对象副本而非引用# 错误存储引用 self._frame_queue frames # frames是外部列表元素被复用 # 正确深拷贝每一帧 self._frame_queue [copy.deepcopy(f) for f in frames]或更高效地在_schedule_next_send()中每次build_can_frame()新建对象。5. DBC解析与信号级发送从原始帧到工程语义的跃迁5.1 为什么需要DBC集成——脱离“猜ID和偏移”的玄学调试在产线测试中你面对的不是0x123, [0x01,0x02,0x03,0x04]而是“设置发动机目标转速为1500rpm”。ECU厂商只提供DBC文件其中定义了EngineSpeed信号位于0x201帧的 bit 16-31比例因子0.125偏移-1000ThrottlePosition位于同一帧的 bit 0-7比例因子0.4无偏移。手动计算1500 → (1500 1000) / 0.125 20000 → 0x4E20再拼接字节效率低且易错。本方案内置轻量DBC解析器支持信号级赋值。5.2 DBC文件精简解析只提取信号位置与缩放参数DBC是文本格式但全量解析复杂。我们只提取关键字段# 文件: dbc_parser.py import re from typing import Dict, List, NamedTuple class SignalInfo(NamedTuple): name: str start_bit: int length: int factor: float offset: float is_signed: bool def parse_dbc_signals(dbc_path: str, frame_id: int) - Dict[str, SignalInfo]: 解析DBC中指定帧ID的所有信号 :param dbc_path: DBC文件路径 :param frame_id: 十进制帧ID如513 :return: {signal_name: SignalInfo} 字典 signals {} frame_hex f{frame_id:X} # 转为大写十六进制字符串 with open(dbc_path, r, encodingutf-8) as f: lines f.readlines() # 步骤1定位到目标帧的BO_ 行 bo_line_idx -1 for i, line in enumerate(lines): if line.startswith(fBO_ {frame_hex} ): bo_line_idx i break if bo_line_idx -1: return signals # 步骤2从BO_行提取信号名列表BS_行之后的SG_行 sg_lines [] for i in range(bo_line_idx 1, len(lines)): line lines[i].strip() if not line or line.startswith(BO_) or line.startswith(CM_): break if line.startswith(SG_): sg_lines.append(line) # 步骤3解析每个SG_行 # 示例: SG_ EngineSpeed : 16|161 (0.125, -1000) [0|16383] rpm Vector__XXX pattern rSG_\s(\w)\s:\s(\d)\|(\d)(\d)(\|-)\s\(([^)])\)\s\[[^]]\]\s([^]*) for sg_line in sg_lines: match re.match(pattern, sg_line) if not match: continue signal_name match.group(1) start_bit int(match.group(2)) length int(match.group(3)) endian match.group(4) # 1 big-endian, 0 little-endian本方案暂不支持LE sign match.group(5) # unsigned, - signed factor_offset match.group(6) # 0.125, -1000 try: factor, offset map(float, [x.strip() for x in factor_offset.split(,)]) except: factor, offset 1.0, 0.0 signals[signal_name] SignalInfo( namesignal_name, start_bitstart_bit, lengthlength, factorfactor, offsetoffset, is_signed(sign -) ) return signals # 使用示例 signals parse_dbc_signals(engine.dbc, 0x201) print(signals[EngineSpeed]) # SignalInfo(nameEngineSpeed, start_bit16, length16, factor0.125, offset-1000, is_signedFalse)关键点正则表达式pattern精确匹配DBC标准格式忽略注释和扩展字段start_bit是从0开始的位索引length是信号位宽如16位本方案仅支持Motorola格式Big-Endian因为PEAK硬件和主流ECU均采用此格式Little-Endian需额外字节序转换暂不支持。5.3 信号值到CAN帧的自动编码位操作与字节填充# 文件: signal_encoder.py def encode_signal_value(value: float, signal: SignalInfo, current_data: bytearray None) - bytearray: 将物理值编码为信号位并写入data字节数组 :param value: 物理值如1500.0 :param signal: SignalInfo对象 :param current_data: 当前帧数据8字节标准CAN或64字节FDNone则新建 :return: 修改后的data字节数组 if current_data is None: data_len 8 if signal.length 64 else 64 current_data bytearray(data_len) # 1. 转换为原始值raw value raw_value int(round((value - signal.offset) / signal.factor)) # 2. 截断到信号位宽范围内 max_unsigned (1 signal.length) - 1 if signal.is_signed: max_signed (1 (signal.length - 1)) - 1 min_signed -(1 (signal.length - 1)) raw_value max(min_signed, min(max_signed, raw_value)) else: raw_value max(0, min(max_unsigned, raw_value)) # 3. 位操作将raw_value写入start_bit开始的length位 # 假设大端序bit 0 是最高位MSB byte_start signal.start_bit // 8 bit_in_byte 7 - (signal.start_bit % 8) # bit_in_byte: 0MSB, 7LSB # 逐位写入简化版实际需处理跨字节 for i in range(signal.length): bit_pos signal.start_bit i byte_idx bit_pos // 8 bit_idx 7 - (bit_pos % 8) if byte_idx len(current_data): continue mask 1 bit_idx if (raw_value (signal.length - 1 - i)) 1: current_data[byte_idx] | mask else: current_data[byte_idx] ~mask return current_data # 使用示例设置EngineSpeed1500rpm signals parse_dbc_signals(engine.dbc, 0x201) data bytearray(8) data encode_signal_value(1500.0, signals[EngineSpeed], data) data encode_signal_value(50.0, signals[ThrottlePosition], data) frame build_can_frame(0x201, bytes(data))参数说明encode_signal_value返回bytearray可链式调用多次设置多个信号位操作逻辑假设大端序Motorolastart_bit16表示第2个字节的MSB跨字节信号如start_bit15, length16需更复杂处理本资源已实现完整跨字节逻辑见源码signal_encoder.py第127行。5.4 集成到自动发送流程从信号名到周期性输出# 文件: dbc_sender.py class DBCPeriodicSender(PeriodicCANSender): def __init__(self, pcan_dll, channel_handle, interval_ms: int, dbc_path: str, frame_id: int): super().__init__(pcan_dll, channel_handle, interval_ms) self.dbc_path dbc_path self.frame_id frame_id self.signals parse_dbc_signals(dbc_path, frame_id) self._current_values {name: 0.0 for name in self.signals.keys()} def set_signal(self, signal_name: str, value: float): 设置信号值非线程安全应在主线程调用 if signal_name not in self.signals: raise KeyError(fSignal {signal_name} not found in DBC) self._current_values[signal_name] value def _generate_frame(self) - TPCANMsg: 根据当前信号值生成帧 data bytearray(8) for name, value in self._current_values.items(): data encode_signal_value(value, self.signals[name], data) return build_can_frame(self.frame_id, bytes(data)) def _schedule_next_send(self): 重写父类方法每次发送前生成新帧 if not self._is_running: return # 生成当前帧 frame self._generate_frame() # 发送 send_can_frame_with_backoff(self.pcan_dll, self.channel_handle, frame) # p a hrefhttps://download.csdn.net/download/dony1843/11168299 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p