Add CAN inputs for drivetrain display values with DBC and bench evidence

This commit is contained in:
shenlefengnai-rgb
2026-10-03 18:53:17 +08:00
parent ceb88a90a5
commit dade0e5d3a
51 changed files with 7084 additions and 51 deletions
+2 -2
View File
@@ -127,7 +127,7 @@ def export(font_path):
canvas.line(40, 31, page_width-40, 31)
canvas.setFont('AMIChinese', 8)
canvas.setFillColor(colors.HexColor('#506775'))
canvas.drawString(40, 19, 'AMI-2.1 | 2026-10-03 | CAN1 500K / CAN2 250K')
canvas.drawString(40, 19, 'AMI-2.2 | 2026-10-03 | CAN1 500K / CAN2 250K')
canvas.drawRightString(page_width-40, 19, f'{doc.page}')
canvas.restoreState()
@@ -136,7 +136,7 @@ def export(font_path):
leftMargin=40, rightMargin=40,
topMargin=36, bottomMargin=44,
title='AMI CAN 报文说明', author='AMI / YMR',
subject='AMI CAN DBC v2.1: CAN1 500K / CAN2 250K')
subject='AMI CAN DBC v2.2: CAN1 500K / CAN2 250K')
document.build(story, onFirstPage=footer, onLaterPages=footer)
print(OUTPUT)
+40 -9
View File
@@ -23,6 +23,7 @@ def can2_dbc_text():
return text
PURPOSE = {
0x182: 'VCU / 测试器 -> AMI:扭矩千分、电机温度、MCU 温度',
0x300: 'AMI -> VCU:用户确认后的任务选择请求',
0x6A0: 'SDC -> AMI:板标识、版本、序号、上电时间',
0x6A1: 'SDC -> AMI:低压电池电压,精确 DLC=2',
@@ -40,7 +41,7 @@ def render():
db = cantools.database.load_file(ROOT / 'AMI.dbc', strict=True)
lines = [
'# AMI CAN DBC 说明', '',
f'版本:`{db.version}`。由 [AMI.dbc](../AMI.dbc) 自动生成,10 条报文、33 个信号。', '',
f'版本:`{db.version}`。由 [AMI.dbc](../AMI.dbc) 自动生成,{len(db.messages)} 条报文、{sum(len(m.signals) for m in db.messages)} 个信号。', '',
'## 使用范围', '',
'CAN1 为 500 kbit/s,CAN2 为 250 kbit/s,均为 Classic CAN、87.5% 采样点。'
'两路保留相同的接收 ID、解码与任务发送功能,MCU 不在两路之间转发原始报文。'
@@ -49,22 +50,24 @@ def render():
'| --- | --- | --- | --- |',
'| CAN1 | 500 kbit/s | PA11 / PA12 | [AMI.dbc](../AMI.dbc) |',
'| CAN2 | 250 kbit/s | PB0 / PB1 | [AMI_CAN2_250k.dbc](../AMI_CAN2_250k.dbc) |', '',
'DBC 的 `Baudrate` / `AMIPort` 分别标注所属总线。每份 DBC 均含 10 条报文、33 个信号;'
'DBC 的 `Baudrate` / `AMIPort` 分别标注所属总线。每份 DBC 均含 11 条报文、40 个信号;'
'同一份报文表适用于两路。CAN 工具必须按接入端口设置速率,导入 DBC 不一定自动改硬件速率。'
'实际接法:SDC 500 kbit/s 接 CAN1,BMS 250 kbit/s 接 CAN2。', '',
'FDCAN 时钟 64 MHz,时钟分频为 1;NominalPrescaler 为 CAN1=8、CAN2=16,'
'两路均使用 Sync=1、TimeSeg1=13、TimeSeg2=2、SJW=1。'
'速率 = 64 MHz / Prescaler / (1+13+2);采样点 = (1+13)/16 = 87.5%。', '',
'本 DBC 包含 AMI 唯一发送的 `0x300` 任务请求,以及 AMI 当前识别的 SDC / BMS 输入。'
'本 DBC 包含 AMI 唯一发送的 `0x300` 任务请求,以及 AMI 当前识别的 SDC / BMS 和自定义显示输入。'
'SDC `0x6A3` DAC 控制、`0x6A5` ASSI 控制、BMS 其他电芯/故障字段不属于此接口子集。'
'没有定义电机温度、逆变器温度、扭矩、标定后的车速、AMI 周期心跳或 VCU 执行反馈。'
'`0x182` 新增扭矩千分、电机温度和电机控制器 MCU 温度的仪表显示输入;'
'发送者负责转换实际电机控制器协议。未定义标定后的车速、AMI 周期心跳或 VCU 执行反馈。'
'旧 `0x180 / 0x181 / 0x280` 输入已停用。', '',
'SDC 字段依据 `SDC_ACQ_DAC.dbc` VERSION 1.1,保持其信号名、位布局、缩放和单位;'
'仅将输入接收节点设为 AMI。BMS 字段依据 '
'[BmsMonitor-CANTest](https://git.acetaffy.ltd/acetaffy/BmsMonitor-CANTest) '
'提交 `e65c95203d1689df05343775982d7e85bd782646` 的 `BmsProtocolParser.cs`。', '',
'DBC 为 ASCII 编码,注释和枚举使用英文,便于导入 CAN 工具;本文为 UTF-8 中文。'
'物理值 = 原始无符号值 × Factor + Offset;下表范围为编码范围,不表示车辆正常工作范围。', '',
'物理值 = 原始值 × Factor + Offset;`0x182` 的三个数值为有符号 int16,其他数值按表定义。'
'下表范围为接口允许范围,不表示车辆正常工作范围。', '',
'## 报文总览', '',
'| CAN ID | 类型 | DLC | 报文名 | 方向和作用 | 周期 / 触发 |',
'| --- | --- | --- | --- | --- | --- |',
@@ -78,7 +81,7 @@ def render():
'', '扩展帧在 DBC 的 `BO_` 十进制 ID 中设置 bit31:'
'`0x186040F3 -> 2556444915`,`0x186140F3 -> 2556510451`。'
'CAN 工具中使用实际 29 位仲裁 ID 并选择扩展帧;bit31 不是总线上发送的仲裁 ID。', '',
'`GenMsgCycleTime` 仅填写已确认的 SDC 周期 100 / 500 / 20 ms。'
'`GenMsgCycleTime` 填写已确认的 SDC 周期 100 / 500 / 20 ms,以及自定义 `0x182` 的约定周期 100 ms。'
'其余帧未填写周期,默认 0 表示未指定,不据此推断 BMS 周期。', '',
'## 信号布局', '',
'Start 为 DBC 起始位,位号从 0 开始。Intel `@1` 从最低有效位起算;'
@@ -100,6 +103,28 @@ def render():
lines += ['', ';'.join(choices) + '。']
lines += ['']
lines += [
'## 自定义动力显示输入 0x182', '',
'标准数据帧,DLC 必须为 8;CAN1 500K 或 CAN2 250K 均可接收,建议每 100 ms 发送。'
'这是一条 AMI 仪表显示设置接口;扭矩千分以发送者定义的基准扭矩归一化,500 表示 500‰。'
'MCU 温度由发送者提供,表示电机控制器 MCU / 逆变器温度,不是 AMI 芯片自动测温。', '',
'| 字节 | 字段 | 编码与规则 |', '| --- | --- | --- |',
'| 0..1 | 扭矩千分 | 小端 int16,允许 -1000..1000,直接显示 |',
'| 2..3 | 电机温度 | 小端 int16 × 0.1 ℃,仪表四舍五入到整数 ℃ |',
'| 4..5 | MCU 温度 | 小端 int16 × 0.1 ℃,仪表四舍五入到整数 ℃ |',
'| 6 | 有效标志 | bit0 扭矩、bit1 电机温度、bit2 MCU 温度;1 有效,0 无效;bit3..7 必须为 0 |',
'| 7 | 帧版本 | 固定 1,与整体接口 v2.2 版本号独立 |', '',
'有效值更新并显示白色;标志为 0 时立即变灰并保留历史数字。'
'扭矩超出 -1000..1000 时仅扭矩无效,两个温度仍按各自标志处理。'
'温度的 int16 编码范围为 -3276.8..3276.7 ℃;显示正负半值均向远离零方向取整。'
'扩展帧、错误 DLC、版本非 1 或保留位非 0 均整帧拒绝,不覆盖数值、不刷新超时。'
'连续 2000 ms 未收到合规帧后,三个值同时变灰;BMS / SDC 流量不能续期。', '',
'CAN 工具发送样例:标准 ID `182`,DLC `8`,数据 `F4 01 C2 01 90 01 07 01`,周期 `100 ms`。'
'可用仓库脚本发送:', '', '```powershell',
'python -m pip install -r scripts/requirements-can.txt',
'python scripts/send_drivetrain_can.py --torque 500 --motor-temp 45 --mcu-temp 40 --dry-run',
'# PCAN 当前接 CAN2,速率 250000;其他适配器按 python-can 接口参数设置',
'python scripts/send_drivetrain_can.py --interface pcan --channel PCAN_USBBUS1 --bitrate 250000 --torque 500 --motor-temp 45 --mcu-temp 40 --duration 10',
'```', '',
'## AMI 任务请求语义', '',
'| Task_ID | 任务 |', '| --- | --- |',
'| 1 | 八字绕环 |', '| 2 | 直线加速 |', '| 3 | 高速循迹 |',
@@ -117,7 +142,10 @@ def render():
'| 低压电池 | `0x6A1` uint16 mV;`lvbat.val=(mV+5)/10`,显示两位小数 V | 1500 ms 无有效 ADC |',
'| 高压电压 | BMS 大端 uint16 × 0.1 V;现有控件四舍五入到整数 V | 2000 ms 无有效 BMS 状态 |',
'| 高压电流 | BMS 大端 uint16 × 0.1 - 500 A;四舍五入到整数 A | 2000 ms 无有效 BMS 状态 |',
'| 最高单体温度 | `0x156` 有效通道最大值,或扩展帧字节 4 - 40 | 全无效或 2000 ms 无有效温度源 |', '',
'| 最高单体温度 | `0x156` 有效通道最大值,或扩展帧字节 4 - 40 | 全无效或 2000 ms 无有效温度源 |',
'| 扭矩千分 | `0x182` 小端 int16,直接更新 `dash.mct.val` | bit0=0、超范围或 2000 ms 超时 |',
'| 电机温度 | `0x182` int16 × 0.1 ℃,整数显示 `dash.djwd.val` | bit1=0 或 2000 ms 超时 |',
'| MCU 温度 | `0x182` int16 × 0.1 ℃,整数显示 `dash.mcuwd.val` | bit2=0 或 2000 ms 超时 |', '',
'电流字段是带偏置的 uint16,不能按 int16 解码。温度原始 `0xFF` 为无效;'
'DBC 枚举将其标为 Invalid。关闭枚举解码的工具可能显示 215,聚合时必须先排除 '
'原始 255,不能把它当作 215 ℃。有效实际温度范围为 -40..214 ℃。'
@@ -131,6 +159,8 @@ def render():
'## 报文样例', '',
'| ID / DLC | 数据(十六进制) | 解码及仪表显示 |', '| --- | --- | --- |',
'| `0x300 / 8` | `01 02 00 01 E8 03 00 00` | 直线加速请求、序号 0、来源 AMI、上电 1000 ms |',
'| `0x182 / 8` | `F4 01 C2 01 90 01 07 01` | 500‰、45.0 ℃、40.0 ℃,三个值有效 |',
'| `0x182 / 8` | `0C FE 9B FF 33 FF 07 01` | -500‰、-10.1 ℃、-20.5 ℃;显示 -500 / -10 / -21 |',
'| `0x6A1 / 2` | `0C 30` | ADC 12300 mV;低压 12.30 V |',
'| `0x6A2 / 8` | `32 00 64 00 96 00 C8 00` | PA0 / PA2 / PA6 / PB14:50 / 100 / 150 / 200 Hz |',
'| `0x140 / 8` | `0F A0 14 03 00 00 00 05` | 400.0 V / +12.3 A;显示 400 / 12 |',
@@ -145,11 +175,12 @@ def render():
'# 若同时检出 SDC 仓库,可额外比较五条输入帧的原始布局:',
'python scripts/verify_dbc.py --sdc-dbc ../SDC_ACQ_DAC/SDC_ACQ_DAC.dbc',
'```', '',
'校验覆盖严格 DBC 解析(长度、信号越界与重叠)、10 个 ID 与固件宏对应、'
'标准/扩展格式、33 个信号、发送/接收节点、SDC 周期、24 个已知报文解码、'
'校验覆盖严格 DBC 解析(长度、信号越界与重叠)、11 个 ID 与固件宏对应、'
'标准/扩展格式、40 个信号、发送/接收节点、SDC / 自定义帧周期、已知报文解码、'
'六个任务号与计数边界编码、大端偏置电流、无效温度枚举、全负温度和截短 DLC。'
'已安装并验证 cantools 44.1.0,接口参考 [cantools 官方文档](https://cantools.readthedocs.io/en/latest/)。', '',
'上述是静态及软件编解码验证,另校验两份 DBC 的速率、相同信号布局及源码 / CubeMX 位时序一致。'
'v2.2 的烧录回读、新帧 CAN2 测试及串口显示核验状态见 [显示 CAN 测试报告](AMI自定义显示CAN测试报告.md)。'
'v2.1 已完成烧录、完整 Flash 回读比对、CAN1 500K 与 CAN2 250K 到官方 HMI 调试器的'
'实物测试,详见 [双 CAN 测试报告](CAN1_CAN2_v2.1测试报告.md)。'
'之前 v2.0 的 CAN 盒 -> 实物 AMI CAN1 / MCU -> USART2 -> COM4 '
+1
View File
@@ -0,0 +1 @@
python-can==4.6.1
+79
View File
@@ -0,0 +1,79 @@
"""Send AMI 0x182 display values through a python-can adapter (no motor actuation)."""
import argparse
from decimal import Decimal, InvalidOperation
import os
import struct
import time
def temperature_raw(value):
try:
scaled = Decimal(value) * 10
if not scaled.is_finite() or scaled != scaled.to_integral_value():
raise ValueError('temperature must have at most one decimal place')
raw = int(scaled)
except (InvalidOperation, OverflowError) as exc:
raise ValueError('invalid temperature') from exc
if not -32768 <= raw <= 32767:
raise ValueError('temperature must be -3276.8..3276.7 degC')
return raw
def build_payload(torque, motor_temp, mcu_temp, valid=7):
if not -1000 <= torque <= 1000:
raise ValueError('torque must be -1000..1000 permille')
if not 0 <= valid <= 7:
raise ValueError('valid must be 0..7 (bit0 torque, bit1 motor, bit2 MCU)')
return struct.pack('<hhhBB', torque, temperature_raw(motor_temp),
temperature_raw(mcu_temp), valid, 1)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--torque', type=int, required=True)
parser.add_argument('--motor-temp', required=True, help='degC, up to one decimal place')
parser.add_argument('--mcu-temp', required=True, help='motor-controller temperature, degC')
parser.add_argument('--valid', type=int, default=7, help='validity mask 0..7, default all valid')
parser.add_argument('--interface', default='pcan')
parser.add_argument('--channel', default='PCAN_USBBUS1')
parser.add_argument('--bitrate', type=int, choices=(250000, 500000), default=250000)
parser.add_argument('--period', type=float, default=0.1, help='seconds, default 100 ms')
parser.add_argument('--duration', type=float, default=10, help='seconds; finite transmission')
parser.add_argument('--dll-dir', help='Windows adapter DLL directory, if required')
parser.add_argument('--dry-run', action='store_true', help='print payload without opening CAN')
args = parser.parse_args()
try:
payload = build_payload(args.torque, args.motor_temp, args.mcu_temp, args.valid)
except ValueError as exc:
parser.error(str(exc))
if not (0 < args.period <= 2 and 0 < args.duration < float('inf')):
parser.error('period must be >0 and <=2 seconds; duration must be positive and finite')
print(f'Standard ID 0x182, DLC 8, data {payload.hex(" ").upper()}, bitrate {args.bitrate}', flush=True)
if args.dry_run:
return
dll_handle = None
if args.dll_dir:
path = os.path.abspath(args.dll_dir)
os.environ['PATH'] = path + os.pathsep + os.environ['PATH']
if hasattr(os, 'add_dll_directory'):
dll_handle = os.add_dll_directory(path)
import can
count = 0
try:
with can.Bus(interface=args.interface, channel=args.channel, bitrate=args.bitrate) as bus:
message = can.Message(arbitration_id=0x182, is_extended_id=False, data=payload)
started = time.monotonic()
deadline = started
while time.monotonic() < started + args.duration:
bus.send(message, timeout=0.2)
count += 1
deadline += args.period
time.sleep(max(0, min(deadline, started + args.duration) - time.monotonic()))
finally:
if dll_handle is not None:
dll_handle.close()
print(f'Sent {count} frames. Values expire after 2 seconds without valid input.')
if __name__ == '__main__':
main()
+36 -6
View File
@@ -11,6 +11,7 @@ import re
import cantools
from export_dbc_docs import can2_dbc_text
from send_drivetrain_can import build_payload
ROOT = Path(__file__).resolve().parents[1]
@@ -36,6 +37,7 @@ def verify_bit_timing():
assert value('ClockDivider') == 'FDCAN_CLOCK_DIV1'
assert value('FrameFormat') == 'FDCAN_FRAME_CLASSIC'
assert int(value('StdFiltersNbr')) == int(ioc[f'FDCAN{port}.StdFiltersNbr']) == 4
prescaler, seg1, seg2, sjw = (int(value(field)) for field in
('NominalPrescaler', 'NominalTimeSeg1',
'NominalTimeSeg2', 'NominalSyncJumpWidth'))
@@ -70,23 +72,24 @@ def main():
header = (ROOT / 'firmware/Core/Inc/ami_protocol.h').read_text(encoding='utf-8')
identifiers = {int(value, 16) for value in re.findall(
r'^#define\s+AMI_CAN_ID_\w+\s+(0x[\dA-Fa-f]+)', header, re.MULTILINE)}
expected = {0x300:(False,8), 0x6A0:(False,8), 0x6A1:(False,2),
expected = {0x182:(False,8), 0x300:(False,8), 0x6A0:(False,8), 0x6A1:(False,2),
0x6A2:(False,8), 0x6A4:(False,4), 0x6A6:(False,8),
0x140:(False,8), 0x156:(False,8),
0x186040F3:(True,8), 0x186140F3:(True,8)}
actual = {m.frame_id:(m.is_extended_frame,m.length) for m in db.messages}
assert actual == expected, actual
assert set(actual) == identifiers, (actual, identifiers)
assert len(db.messages) == 10
assert sum(len(m.signals) for m in db.messages) == 33
assert len(db.messages) == 11
assert sum(len(m.signals) for m in db.messages) == 40
for m in db.messages:
if m.frame_id == 0x300:
assert m.senders == ['AMI'] and m.receivers == {'VCU'}
else:
assert m.receivers == {'AMI'}
assert m.senders == (['SDC_ACQ_DAC'] if 0x6A0 <= m.frame_id <= 0x6A6 else ['BMS'])
assert m.senders == (['VCU'] if m.frame_id == 0x182 else
['SDC_ACQ_DAC'] if 0x6A0 <= m.frame_id <= 0x6A6 else ['BMS'])
assert db.dbc.attributes['Baudrate'].value == 500000
for ident, cycle in [(0x6A0,100), (0x6A1,500), (0x6A2,20)]:
for ident, cycle in [(0x182,100), (0x6A0,100), (0x6A1,500), (0x6A2,20)]:
assert db.get_message_by_frame_id(ident).cycle_time == cycle
for ident in [0x300,0x6A4,0x6A6,0x140,0x156,0x186040F3,0x186140F3]:
assert db.get_message_by_frame_id(ident).cycle_time in (None,0)
@@ -96,6 +99,32 @@ def main():
checks.append({'id':hex(ident),'data':data})
return db.decode_message(ident, bytes.fromhex(data), decode_choices=False)
display = db.get_message_by_frame_id(0x182)
for raw, torque, motor, mcu, flags in [
('F4 01 C2 01 90 01 07 01',500,45,40,7),
('0C FE 9B FF 33 FF 07 01',-500,-10.1,-20.5,7),
('18 FC 00 80 FF 7F 05 01',-1000,-3276.8,3276.7,5),
('E8 03 00 00 00 00 00 01',1000,0,0,0)]:
values = decode(0x182,raw)
assert values['Torque_Permille'] == torque
near(values['Motor_Temperature'],motor)
near(values['MCU_Temperature'],mcu)
for bit, name in enumerate(('Torque_Valid','Motor_Temperature_Valid','MCU_Temperature_Valid')):
assert values[name] == ((flags >> bit) & 1)
assert values['Protocol_Version'] == 1
assert display.encode(values) == bytes.fromhex(raw)
assert build_payload(torque,str(motor),str(mcu),flags) == bytes.fromhex(raw)
assert all(display.get_signal_by_name(name).is_signed for name in
('Torque_Permille','Motor_Temperature','MCU_Temperature'))
for arguments in ((1001,'45','40',7), (0,'3276.8','40',7),
(0,'45.01','40',7), (0,'45','40',8), (0,'nan','40',7)):
try:
build_payload(*arguments)
except ValueError:
pass
else:
raise AssertionError(f'Invalid sender arguments accepted: {arguments}')
# Canonical request at 1000 ms, first sequence, task 2; all six task enums.
task = decode(0x300,'01 02 00 01 E8 03 00 00')
assert task == {'Protocol_Version':1,'Task_ID':2,'Request_Sequence':0,
@@ -189,11 +218,12 @@ def main():
for decode_choices in (False, True):
assert db.decode_message(ident, payload, decode_choices=decode_choices) == \
db2.decode_message(ident, payload, decode_choices=decode_choices)
print(json.dumps({'result':'passed','messages':len(db.messages),'signals':33,
print(json.dumps({'result':'passed','messages':len(db.messages),'signals':40,
'known_payload_checks':len(checks),'all_task_ids_and_counter_edges':True,
'ports_bitrate':bitrates,'both_dbc_layouts_and_payloads_match':True,
'firmware_and_ioc_bit_timing_match':True,
'firmware_ids_match':True,'sdc_source_layout_match':source_check,
'display_sender_known_bytes_and_argument_rejection':True,
'cantools_version':cantools.__version__},indent=2))