test/freetest/up/CAN_程序说明.md

341 lines
14 KiB
Markdown

# CAN.py 程序说明文档
## 一、程序概述
`CAN.py` 是一个基于 Python 的 **CAN 通信上位机程序**,用于通过 USB-CAN 适配器与 STM32 单片机进行 CAN 总线通信。其核心功能是实现 **UDS(统一诊断服务)BootLoader**,支持向单片机发送诊断指令和烧写 BIN 固件。
### 适用硬件
- **CAN 适配器**:创芯科技 CANalyst-II / USBCAN-2 系列(设备类型 `VCI_USB_CAN_2 = 4`)
- **下位机**:STM32F429IGT6(BootLoader 标题所示,可适配其他 STM32)
- **依赖 DLL**:`ControlCAN.dll`(需与脚本同目录)
### 技术栈
| 模块 | 用途 |
|------|------|
| `tkinter` | 构建 GUI 图形界面 |
| `ctypes` | 调用 `ControlCAN.dll` 中的 C 函数 |
| `threading` | 子线程接收 CAN 数据,避免阻塞主界面 |
| `os` / `binascii` | 文件读取与字节转换 |
---
## 二、程序结构
```
CAN.py
├── 导入与 DLL 加载 (1~22行)
├── ToolTip 工具提示类 (24~64行)
├── GUI 窗口构建 g_WindowStart (72~162行)
├── 按钮回调函数 (165~213行)
│ ├── FUNC_8bitCANSend 发送 8 位 CAN 指令
│ └── FUNC_BINSend 发送 BIN 固件
├── CAN 基础操作 (220~340行)
│ ├── connect 打开设备
│ ├── init 初始化通道
│ └── start 启动通道
├── CAN 收发函数 (348~753行)
│ ├── transmitBIN 发送单帧数据
│ ├── transmit 发送默认数据
│ ├── receive_by_thread 接收线程入口
│ └── receive 接收并解析 UDS 响应
├── CAN_Start 初始化+启动通道
├── ReadFile 读取并发送 BIN 文件
└── __main__ 程序入口 (831~857行)
```
---
## 三、关键配置参数
### 1. 设备与通道参数([CAN.py:222-226](file:///d:/00_software/00_keil/01_keil_file/001_BMS/BMS_SLAVE/000_update/freertos/freetest/up/CAN.py#L222-L226))
| 参数 | 值 | 说明 |
|------|-----|------|
| `VCI_USB_CAN_2` | 4 | CAN 卡类别(USBCAN-2A/2C/CANalyst-II) |
| `DEV_INDEX` | 0 | 设备索引(第一个适配器为 0) |
| `STATUS_OK` | 1 | 返回值 1=成功,0=失败 |
### 2. CAN 初始化参数([CAN.py:271-284](file:///d:/00_software/00_keil/01_keil_file/001_BMS/BMS_SLAVE/000_update/freertos/freetest/up/CAN.py#L271-L284))
| 参数 | 值 | 说明 |
|------|-----|------|
| `ACC_CODE` | 0x80000000 | 过滤验收码 |
| `ACC_MASK` | 0xFFFFFFFF | 过滤屏蔽码(接收所有) |
| `FILTER` | 0 | 滤波模式:接收所有类型帧 |
| `TIMING_0` | 0x00 | 波特率 T0,**500kbps** |
| `TIMING_1` | 0x1C | 波特率 T1 |
| `MODE` | 0 | 工作模式:正常工作 |
> **波特率说明**:Timing0=0x00, Timing1=0x1C 对应 **500kbps**,与 BMS 项目下位机配置一致。
### 3. 帧结构参数([CAN.py:371-399](file:///d:/00_software/00_keil/01_keil_file/001_BMS/BMS_SLAVE/000_update/freertos/freetest/up/CAN.py#L371-L399))
| 参数 | 值 | 说明 |
|------|-----|------|
| `TRANSMIT_ID` | 0x1 | 发送帧 ID |
| `RECEIVE_ID` | 0x0 | 接收帧 ID |
| `TRANSMIT_SEND_TYPE` | 1 | 单次发送(失败不重发,响应快) |
| `REMOTE_FLAG` | 0 | 数据帧 |
| `EXTERN_FLAG` | 0 | 标准帧(11 位 ID) |
| `DATA_LEN` | 8 | 数据长度 DLC |
| `RECEIVE_LEN` | 2500 | 接收缓存区长度 |
| `TRANSMIT_LEN` | 1 | 每次发送单帧 |
### 4. DLL 路径配置([CAN.py:20](file:///d:/00_software/00_keil/01_keil_file/001_BMS/BMS_SLAVE/000_update/freertos/freetest/up/CAN.py#L20))
```python
CAN_DLL_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'ControlCAN.dll')
```
> 已修改为基于脚本所在目录定位 DLL,任意工作目录运行均可找到。
---
## 四、核心函数详解
### 4.1 GUI 窗口 `g_WindowStart()` ([CAN.py:72](file:///d:/00_software/00_keil/01_keil_file/001_BMS/BMS_SLAVE/000_update/freertos/freetest/up/CAN.py#L72))
构建主界面,包含:
- **CAN 指令输入框**:默认 `0x10 0x02 0x00 0x00 0x00 0x00 0x00 0x00`
- **BIN 固件路径输入框**
- **两个按钮**:发送 8 位指令 / 发送 BIN 固件
- **日志文本框**:黑色背景绿色字体,显示通信日志
启动流程:
1. 创建界面 → 2. `connect()` 打开设备 → 3. `CAN_Start()` 初始化并启动通道 → 4. 启动接收线程 → 5. `win.mainloop()` 进入消息循环
### 4.2 CAN 设备控制
| 函数 | DLL 接口 | 作用 |
|------|----------|------|
| `connect()` | `VCI_OpenDevice` | 打开 USB-CAN 设备 |
| `init(can_index)` | `VCI_InitCAN` | 初始化指定 CAN 通道(设置波特率、滤波等) |
| `start(can_index)` | `VCI_StartCAN` | 启动指定 CAN 通道 |
| `CAN_Start()` | - | 依次调用 init + start 启动通道 1 |
### 4.3 数据发送
#### `transmitBIN(can_index, BUFF, Sendstr)` ([CAN.py:402](file:///d:/00_software/00_keil/01_keil_file/001_BMS/BMS_SLAVE/000_update/freertos/freetest/up/CAN.py#L402))
- **作用**:发送 8 字节数据帧
- **参数**:
- `BUFF`:8 字节数据列表
- `Sendstr`:用于日志显示的原始字符串
- **流程**:构造 `VCI_CAN_OBJ` 结构体 → 调用 `VCI_Transmit` → 日志显示结果
#### `ReadFile(BIN_PATH)` ([CAN.py:762](file:///d:/00_software/00_keil/01_keil_file/001_BMS/BMS_SLAVE/000_update/freertos/freetest/up/CAN.py#L762))
- **作用**:读取 BIN 固件并按 8 字节分包发送
- **流程**:
1. 打开二进制文件,获取文件大小
2. 循环读取字节,每凑齐 8 字节调用 `transmitBIN` 发送一帧
3. 每帧间隔 `time.sleep(0.001)`(1ms)
4. 文件读取完毕后发送最后一包(不足 8 字节补 0)
### 4.4 数据接收
#### `receive_by_thread(can_index)` ([CAN.py:461](file:///d:/00_software/00_keil/01_keil_file/001_BMS/BMS_SLAVE/000_update/freertos/freetest/up/CAN.py#L461))
- **作用**:独立子线程持续轮询接收 CAN 数据
- **特点**:每 1ms 调用一次 `receive()`,异常时退出线程
#### `receive(can_index)` ([CAN.py:478](file:///d:/00_software/00_keil/01_keil_file/001_BMS/BMS_SLAVE/000_update/freertos/freetest/up/CAN.py#L478))
- **作用**:接收一帧数据并解析 UDS 响应
- **流程**:
1. 调用 `VCI_Receive` 读取数据
2. 接收失败时循环重试(无超时退出,已被注释)
3. 接收成功后打印 ID、DataLen、Data
4. 根据首字节(SID)进入不同的 UDS 响应解析分支
---
## 五、UDS 指令解析
程序通过 `receive()` 函数中的 if-else 分支解析下位机的 UDS 响应。响应帧首字节为服务 ID(SID)。
### 5.1 应用程序指令集(诊断会话控制)
| 首字节 | 次字节 | 含义 |
|--------|--------|------|
| `0x7f` | - | 不支持的指令(否定响应) |
| `0x50` | `0x03` | 进入扩展会话模式成功 |
| `0x50` | `0x01` | 进入默认 01 会话模式成功 |
| `0xc5` | `0x02` | 关闭 DTC 成功 |
| `0xc5` | `0x01` | 开启 DTC 成功 |
| `0x68` | `0x03` | 禁止非诊断报文收发成功 |
| `0x68` | `0x01` | 允许非诊断报文收发成功 |
| `0x54` | - | 清除诊断信息成功 |
### 5.2 Boot 指令集(BootLoader 烧写流程)
| 首字节 | 次字节 | 含义 |
|--------|--------|------|
| `0x50` | `0x02` | 进入编程会话模式(30 秒内需执行烧写指令) |
| `0x62` | - | 读取 DID 数据成功 |
| `0x67` | `0x01` | 安全访问:获取 seed 成功 |
| `0x67` | `0x02` | 安全访问:验证 key 成功(解锁) |
| `0x6e` | - | 写入 DID 数据成功 |
| `0x71` | `0x01` | 执行 RID 成功(擦除/完整性校验) |
| `0x74` | - | 请求下载成功(返回最大数据块长度) |
| `0x76` | `0x01` | 传输完成 |
| `0x77` | - | 退出传输完成 |
| `0x51` | `0x55` | 跳转 APP(烧写完成,跳转到应用程序) |
### 5.3 RID 执行结果细分([CAN.py:688-693](file:///d:/00_software/00_keil/01_keil_file/001_BMS/BMS_SLAVE/000_update/freertos/freetest/up/CAN.py#L688-L693))
| RID[2] | RID[3] | 含义 |
|--------|--------|------|
| `0x10` | `0x05` | 擦除 Memory |
| `0x15` | `0x55 0x77` | 检查完整性:简单校验 OK |
| `0x15` | `0x55 0x66` | 检查完整性:简单校验 ERR |
---
## 六、数据结构
### `VCI_CAN_INIT_CONFIG` 初始化结构体([CAN.py:261](file:///d:/00_software/00_keil/01_keil_file/001_BMS/BMS_SLAVE/000_update/freertos/freetest/up/CAN.py#L261))
```c
struct VCI_CAN_INIT_CONFIG {
UINT AccCode; // 过滤验收码
UINT AccMask; // 过滤屏蔽码
UINT Reserved; // 保留字段
UCHAR Filter; // 滤波模式
UCHAR Timing0; // 波特率 T0
UCHAR Timing1; // 波特率 T1
UCHAR Mode; // 工作模式
};
```
### `VCI_CAN_OBJ` 帧结构体([CAN.py:358](file:///d:/00_software/00_keil/01_keil_file/001_BMS/BMS_SLAVE/000_update/freertos/freetest/up/CAN.py#L358))
```c
struct VCI_CAN_OBJ {
UINT ID; // 帧 ID(右对齐)
UINT TimeStamp; // 时间标识(单位 0.1ms)
UCHAR TimeFlag; // 是否使用时间标识
UCHAR SendType; // 发送类型(0=正常,1=单次)
UCHAR RemoteFlag; // 远程帧标志(0=数据帧,1=远程帧)
UCHAR ExternFlag; // 扩展帧标志(0=标准帧,1=扩展帧)
UCHAR DataLen; // 数据长度 DLC(<=8)
UCHAR Data[8]; // 数据内容
UCHAR Reserved[3]; // 保留字段
};
```
---
## 七、典型工作流程
### 7.1 程序启动流程
```
__main__
└── g_WindowStart()
├── 创建 GUI 界面
├── connect() # 打开 USB-CAN 设备
├── CAN_Start()
│ ├── init(0) # 初始化通道 1(500kbps)
│ └── start(0) # 启动通道 1
├── 启动接收线程
└── win.mainloop() # 进入消息循环
```
### 7.2 发送 8 位 CAN 指令流程
```
用户点击"发送8位指令"按钮
└── FUNC_8bitCANSend()
├── 解析输入字符串为 8 字节十六进制数组
└── transmitBIN()
└── VCI_Transmit() # 调用 DLL 发送
```
### 7.3 烧写 BIN 固件流程
```
用户点击"发送BIN固件"按钮
└── FUNC_BINSend()
└── ReadFile(路径)
├── 打开 BIN 文件
├── 循环读取字节(每 8 字节一帧)
├── transmitBIN() 发送每一帧
└── 间隔 1ms 避免总线拥塞
```
### 7.4 接收解析流程
```
接收线程 (daemon=True)
└── receive_by_thread()
└── while True:
├── sleep(1ms)
└── receive()
├── VCI_Receive() 读取数据
├── 失败:循环重试
└── 成功:按 SID 分支解析 UDS 响应
└── 更新日志文本框
```
---
## 八、运行环境与依赖
### 必需文件
- `CAN.py` - 主程序
- `ControlCAN.dll` - CAN 适配器驱动 DLL(需与脚本同目录)
- `ControlCAN.lib` - 配套库文件
### Python 依赖
- Python 3.x
- 标准库:`tkinter`、`ctypes`、`threading`、`os`、`binascii`、`time`、`struct`、`re`、`urllib.request`、`inspect`
- 第三方库:`pywin32`(`win32com.client`,**注:实际代码未使用,可删除该导入**)
### 运行命令
```powershell
& C:/Users/gjf/AppData/Local/Programs/Python/Python313/python.exe d:/00_software/00_keil/01_keil_file/001_BMS/BMS_SLAVE/000_update/freertos/freetest/up/CAN.py
```
---
## 九、已知问题与改进建议
### 9.1 已知问题
1. **无用导入**([CAN.py:8-9](file:///d:/00_software/00_keil/01_keil_file/001_BMS/BMS_SLAVE/000_update/freertos/freetest/up/CAN.py#L8-L9))
- `from win32com.client import Dispatch``import win32com.client` 未被使用,可删除以减少依赖。
2. **RID 校验逻辑错误**([CAN.py:690-693](file:///d:/00_software/00_keil/01_keil_file/001_BMS/BMS_SLAVE/000_update/freertos/freetest/up/CAN.py#L690-L693))
- 第 690 行 `t_ReceiveData[3] == 0x55 and t_ReceiveData[3] == 0x77` 同一变量不可能同时等于两个值,应为 `t_ReceiveData[3] == 0x77`
- 第 692 行 `t_ReceiveData[3] == 0x55 and t_ReceiveData[3] == 0x66` 同理,应为 `t_ReceiveData[3] == 0x66`
3. **接收无超时退出**([CAN.py:493-502](file:///d:/00_software/00_keil/01_keil_file/001_BMS/BMS_SLAVE/000_update/freertos/freetest/up/CAN.py#L493-L502))
- `receive()` 中接收失败时无限循环重试,超时退出代码已被注释(原 500 次重试)。可能导致界面假死。
4. **BIN 文件路径硬编码默认值**([CAN.py:123](file:///d:/00_software/00_keil/01_keil_file/001_BMS/BMS_SLAVE/000_update/freertos/freetest/up/CAN.py#L123))
- 默认路径 `C:\Users\HenchYoung\Desktop\LED2000.bin` 为原作者路径,需手动修改。
5. **发送帧 ID 固定**([CAN.py:375](file:///d:/00_software/00_keil/01_keil_file/001_BMS/BMS_SLAVE/000_update/freertos/freetest/up/CAN.py#L375))
- `TRANSMIT_ID = 0x1` 固定,无法在界面修改。
### 9.2 改进建议
- 删除无用的 `win32com` 导入,避免强制安装 `pywin32`
- 修复 RID 校验的逻辑错误
- 增加接收超时退出机制,避免界面假死
- BIN 文件路径支持文件选择对话框(`filedialog.askopenfilename`)
- 发送帧 ID 支持界面输入
- 接收线程增加异常恢复机制
- 日志增加文件保存功能,便于排查问题
- 与 BMS 项目对接时,需确认 CAN 通信参数(波特率、帧 ID)与下位机一致
---
## 十、与 BMS 项目的关系
当前 BMS 项目下位机配置(参考项目内存约束):
- **波特率**:500kbps(与本上位机 `TIMING_0/1` 一致)
- **支持标准帧 ID**:`0x100` / `0x101` / `0x180` / `0x181` / `0x182`
- **DLC 转发**:按原始帧 DLC 转发(符合 CANopen 协议)
- **1ms 帧率无丢帧**
> **注意**:本上位机 `TRANSMIT_ID = 0x1`,与 BMS 下位机支持的帧 ID(0x100/0x101/0x180/0x181/0x182)**不一致**。若用于 BMS 项目调试,需修改 [CAN.py:375](file:///d:/00_software/00_keil/01_keil_file/001_BMS/BMS_SLAVE/000_update/freertos/freetest/up/CAN.py#L375) 的 `TRANSMIT_ID` 为对应值,或改用创芯科技官方上位机软件。
---
*文档生成时间:2026-08-06*
*文档作者:基于 CAN.py 源码分析生成*