feat: add AS status feedback for both HMI sizes with verified v2.7 release

This commit is contained in:
shenlefengnai-rgb
2026-10-07 16:32:42 +08:00
parent dba8aa5ea9
commit e6556fc218
245 changed files with 21470 additions and 159 deletions
+4 -3
View File
@@ -39,7 +39,8 @@ def export(font_path):
page_width, page_height = landscape(A4)
width = page_width - 80
body = ParagraphStyle('Body', fontName='AMIChinese', fontSize=9.3,
leading=15, spaceAfter=8, wordWrap='CJK', alignment=TA_LEFT)
leading=14, spaceAfter=6, wordWrap='CJK', alignment=TA_LEFT,
allowWidows=0, allowOrphans=0)
heading = ParagraphStyle('Heading', parent=body, fontSize=16, leading=23,
textColor=colors.HexColor('#12485E'), spaceBefore=3,
spaceAfter=12, keepWithNext=True)
@@ -127,7 +128,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.5 | 2026-10-04 | CAN1 500K / CAN2 250K')
canvas.drawString(40, 19, 'AMI-2.7 | 2026-10-07 | CAN1 500K / CAN2 250K')
canvas.drawRightString(page_width-40, 19, f'{doc.page}')
canvas.restoreState()
@@ -136,7 +137,7 @@ def export(font_path):
leftMargin=40, rightMargin=40,
topMargin=36, bottomMargin=44,
title='AMI CAN 报文说明', author='AMI / YMR',
subject='AMI CAN DBC v2.5: CAN1 500K / CAN2 250K')
subject='AMI CAN DBC v2.7: CAN1 500K / CAN2 250K')
document.build(story, onFirstPage=footer, onLaterPages=footer)
print(OUTPUT)
+56 -31
View File
@@ -30,12 +30,12 @@ def can2_dbc_text():
PURPOSE = {
0x301: 'AMI -> VCU(仅 CAN1):版本、序号、运行时间',
0x183: 'VCU -> AMI:实际当前任务、锁定状态、EBS 状态',
0x183: 'VCU -> AMI:实际当前任务、锁定状态、无人状态、EBS 状态',
0x182: 'VCU / 测试器 -> AMI:扭矩千分、电机温度、MCU 温度',
0x300: 'AMI -> VCU:用户确认后的任务选择请求',
0x6A0: 'SDC -> AMI:板标识、版本、序号、上电时间',
0x6A1: 'SDC -> AMI:低压电池电压,精确 DLC=2',
0x6A2: 'SDC -> AMI:PA0 / PA2 / PA6 / PB14 原始频率',
0x6A2: 'SDC -> AMI:左前 PA0 / 右前 PA6 已标定轮速',
0x6A4: 'SDC -> AMI:DAC 设置应答,识别但不显示',
0x6A6: 'SDC -> AMI:PD0..PD14 原始电平和变化掩码',
0x140: 'BMS -> AMI:高压总电压、带偏置的总电流',
@@ -67,9 +67,9 @@ def render():
'本 DBC 包含 AMI 发送的 `0x300` 任务请求和 `0x301` 周期心跳,以及 AMI 当前识别的 SDC / BMS 和自定义显示输入。'
'SDC `0x6A3` DAC 控制、`0x6A5` ASSI 控制、BMS 其他电芯/故障字段不属于此接口子集。'
'`0x182` 新增扭矩千分、电机温度和电机控制器 MCU 温度的仪表显示输入;'
'发送者负责转换实际电机控制器协议。`0x183` 为新增 VCU 状态反馈,需 VCU 按此自定义接口发送;未定义标定后的车速。'
'发送者负责转换实际电机控制器协议。`0x183` 为新增 VCU 状态反馈,需 VCU 按此自定义接口发送;车速取 SDC 双前轮轮速平均值。'
'旧 `0x180 / 0x181 / 0x280` 输入已停用。', '',
'SDC 字段依据 `SDC_ACQ_DAC.dbc` VERSION 1.1,保持其信号名、位布局、缩放和单位;'
'SDC 字段依据 `SDC_ACQ_DAC.dbc` VERSION 1.1,核对远端 master 提交 `7bc2d9f1eaaf19814957037bd097cd879cbf4cbf`(2026-10-05),保持其信号名、位布局、缩放和单位;'
'仅将输入接收节点设为 AMI。BMS 字段依据 '
'[BmsMonitor-CANTest](https://git.acetaffy.ltd/acetaffy/BmsMonitor-CANTest) '
'提交 `e65c95203d1689df05343775982d7e85bd782646` 的 `BmsProtocolParser.cs`。', '',
@@ -81,7 +81,7 @@ def render():
'| CAN ID | 类型 | DLC | 报文名 | 方向和作用 | 周期 / 触发 |',
'| --- | --- | --- | --- | --- | --- |',
]
triggers = {0x300:'确认任务触发',0x6A4:'DAC 设置后应答',0x6A6:'启动 / 电平变化'}
triggers = {0x300:'确认任务触发',0x6A4:'DAC 设置后应答'}
for m in db.messages:
cycle = f'{m.cycle_time} ms' if m.cycle_time else triggers.get(m.frame_id, '源协议未指定')
lines.append(f'| `0x{m.frame_id:X}` | {"扩展" if m.is_extended_frame else "标准"} | '
@@ -90,7 +90,7 @@ def render():
'', '扩展帧在 DBC 的 `BO_` 十进制 ID 中设置 bit31:'
'`0x186040F3 -> 2556444915`,`0x186140F3 -> 2556510451`。'
'CAN 工具中使用实际 29 位仲裁 ID 并选择扩展帧;bit31 不是总线上发送的仲裁 ID。', '',
'`GenMsgCycleTime` 填写已确认的 SDC 周期 100 / 500 / 20 ms,以及 `0x182`、`0x183` 和 `0x301` 的约定周期 100 ms。'
'`GenMsgCycleTime` 填写已确认的 SDC 周期 100 / 500 / 20 / 1000 ms,以及 `0x182`、`0x183` 和 `0x301` 的约定周期 100 ms。'
'其余帧未填写周期,默认 0 表示未指定,不据此推断 BMS 周期。', '',
'## 信号布局', '',
'Start 为 DBC 起始位,位号从 0 开始。Intel `@1` 从最低有效位起算;'
@@ -112,6 +112,17 @@ def render():
lines += ['', ';'.join(choices) + '。']
lines += ['']
lines += [
'## SDC 轮速与车速显示 0x6A2', '',
'2026-10-05 的 SDC master 将旧四路 Hz 改为双前轮已标定轮速:标准 Classic CAN 数据帧,DLC 必须为 4,每 20 ms 一帧。'
'字节 0..1 为左前 PA0,字节 2..3 为右前 PA6,均为小端 uint16 mm/s(DBC 0.001 m/s)。'
'SDC 已使用每圈 49 脉冲、轮周长 1414 mm,AMI 不再次标定。', '',
'AMI 用双前轮平均轮速作为仪表车速,转换为 km/h 后一次四舍五入:'
'`dash.speed.val=((left_raw+right_raw)*9+2500)/5000`,全部采用 uint32 整数运算。'
'有效时白色;超过 500 ms 没有合规轮速帧时保留历史值并变灰;新帧自动恢复。'
'其他 SDC 心跳、GPIO、BMS 或 VCU 流量不延长车速有效期。旧 DLC 8、扩展、远程或 FD 帧拒绝。', '',
'该源未提供每轮有效标志,0 为有效停止读数,65535 为有效饱和值;不能据此判断传感器健康。'
'SDC 超过 1 秒没有边沿会把对应轮速归零;AMI 仍会把持续收到的零作为有效数值。'
'平均轮速不是 VCU 融合车速,单轮丢脉冲或打滑会影响显示。接收长度变更与旧固件不兼容,需配套 SDC 最新版本。', '',
'## 自定义动力显示输入 0x182', '',
'标准数据帧,DLC 必须为 8;CAN1 500K 或 CAN2 250K 均可接收,建议每 100 ms 发送。'
'这是一条 AMI 仪表显示设置接口;扭矩千分以发送者定义的基准扭矩归一化,500 表示 500‰。'
@@ -136,44 +147,55 @@ def render():
'```', '',
'## VCU 状态反馈 0x183', '',
'新增自定义标准数据帧,DLC 必须为 8,建议周期 100 ms;CAN1 500K 与 CAN2 250K 共用布局。'
'只有该帧更新当前任务、锁定、EBS 主页面显示。AMI 发出任务请求后保留 VCU 上次反馈,等待新的实际状态。', '',
'v2.7 扩展此帧为版本 2,只有该帧更新当前任务、锁定、无人状态和 EBS 主页面显示。AMI 发出任务请求后保留 VCU 上次反馈,等待新的实际状态。', '',
'| 字节 | 字段 | 编码与规则 |', '| --- | --- | --- |',
'| 0 | 当前任务 | 0 无;1 八字绕环;2 直线加速;3 高速循迹;4 车检测试;5 EBS 测试;6 手动驾驶 |',
'| 1 | 锁定 | 0 已解锁(红);1 已锁定(绿) |',
'| 2 | EBS 正常 | 0 故障(红底);1 正常(绿底) |',
'| 3 | 有效标志 | bit0 当前任务,bit1 锁定,bit2 EBS;1 有效,0 未知;bit3..7 为 0 |',
'| 3 | 有效标志 | bit0 当前任务,bit1 锁定,bit2 EBS,bit3 无人状态;1 有效,0 未知;bit4..7 为 0 |',
'| 4 | 反馈计数 | 0..255,供观察;不去重,不作为请求 ACK |',
'| 5..6 | 保留 | 必须为 0 |',
'| 7 | 帧版本 | 固定 1 |', '',
'例:`02 01 01 07 00 00 00 01` 表示当前直线加速、已锁定、EBS 正常。'
'`00 00 00 07 01 00 00 01` 表示无任务、已解锁、EBS 故障。任务 1..6 为绿字,0 无任务为红字。'
'各有效标志为 0 时,对应项立即灰色未知;连续超过 500 ms 无合规帧后三项均未知,其他 CAN 流量不能续期。'
'| 5 | 无人状态 | 0 AS OFF;1 AS Ready;2 AS Driving;3 AS Finished;4 AS Emergency;5 Manual Driving |',
'| 6 | 保留 | 必须为 0 |',
'| 7 | 帧版本 | 固定 2;旧版本 1 兼容规则见下文 |', '',
'例:`02 01 01 0F 00 01 00 02` 表示当前直线加速、已锁定、AS Ready、EBS 正常。'
'`00 00 00 0F 01 00 00 02` 表示无任务、已解锁、AS OFF、EBS 故障。任务 1..6 为绿字,0 无任务为红字。'
'各有效标志为 0 时,对应项立即灰色未知;连续超过 500 ms 无合规帧后四项均未知,其他 CAN 流量不能续期。'
'错误 DLC、版本、枚举值、保留位或扩展/远程/FD 格式整帧拒绝,不改变状态、不续期。'
'该反馈只驱动显示,EBS 安全动作及车辆任务裁决由 VCU 实现。', '',
'### 无人状态来源与兼容', '',
'状态语义依据用户提供 ASF 的 AMI 操作界面及 AS 状态描述。ASF 没有分配本帧的数值编码,'
'上表 0..5 是 AMI 与 VCU 的自定义约定,VCU 需显式实现。Manual Driving 用于显示 ASF 的有人驾驶分支,不能由任务 6 自动推断。', '',
'主页面在已锁定/已解锁右侧显示独立状态:AS OFF 白色,AS Ready 黄色,AS Driving 黄色,'
'AS Finished 蓝色,AS Emergency 红色,Manual Driving 绿色;未知灰色。文字不闪烁,颜色为仪表约定,不代替 ASSI 灯控制。', '',
'旧版本 1(byte7=01)仍接受原三项,要求 byte3 bit3..7=0、byte5=0、byte6=0,'
'每次收到后无人状态立即置未知;不能保留上一条版本 2 的 AS 状态。旧 MCU 不能识别版本 2;'
'新增状态需配套 v2.7 MCU 和包含 as_state 的屏幕工程。800×480 与 480×272 工程均已更新;小屏 AS=5 显示“有人驾驶”。任务、锁定、EBS 与 AS 独立反馈,AMI 不推断安全状态。', '',
'发送工具默认版本 2;指定 --as-state 时默认有效标志 15,未指定时为 7(AS 未知)。'
'--valid 可单独清除各字段有效位;兼容预览可用 --version 1,不能同时指定 --as-state。', '',
'### 当前任务发送对照', '',
'下表第 1、2 字节均为 01,只用于展示已锁定、EBS 正常;VCU 必须填写实际状态。'
'每条合规帧同时更新三项及其有效性,不能只改任务有效位而期望另外两项继续保留。', '',
'下表第 1、2 字节均为 01,并且 byte5=01,只用于展示已锁定、AS Ready、EBS 正常;VCU 必须填写实际状态。'
'每条合规帧同时更新四项及其有效性,不能只改任务有效位而期望其他项继续保留。', '',
'| 任务号 | 当前任务 | 标准 0x183 / DLC 8 数据 |', '| --- | --- | --- |',
'| 0 | 无(红) | `00 01 01 07 00 00 00 01` |',
'| 1 | 八字绕环 | `01 01 01 07 00 00 00 01` |',
'| 2 | 直线加速 | `02 01 01 07 00 00 00 01` |',
'| 3 | 高速循迹 | `03 01 01 07 00 00 00 01` |',
'| 4 | 车检测试 | `04 01 01 07 00 00 00 01` |',
'| 5 | EBS 测试 | `05 01 01 07 00 00 00 01` |',
'| 6 | 手动驾驶 | `06 01 01 07 00 00 00 01` |', '',
'任务未知通过 byte3 的 bit0=0 编码。例如 `00 01 01 06 00 00 00 01`'
'显示任务未知、已锁定、EBS 正常;不要发送任务号 7,7 是屏内未知状态而不是 CAN 枚举。'
'任务号、锁定、EBS 的数值范围始终校验,有效标志为 0 时也不能发送越界值。', '',
'| 0 | 无(红) | `00 01 01 0F 00 01 00 02` |',
'| 1 | 八字绕环 | `01 01 01 0F 00 01 00 02` |',
'| 2 | 直线加速 | `02 01 01 0F 00 01 00 02` |',
'| 3 | 高速循迹 | `03 01 01 0F 00 01 00 02` |',
'| 4 | 车检测试 | `04 01 01 0F 00 01 00 02` |',
'| 5 | EBS 测试 | `05 01 01 0F 00 01 00 02` |',
'| 6 | 手动驾驶 | `06 01 01 0F 00 01 00 02` |', '',
'任务未知通过 byte3 的 bit0=0 编码。例如 `00 01 01 0E 00 01 00 02`'
'显示任务未知、已锁定、AS Ready、EBS 正常;不要发送任务号 7,7 是屏内未知状态而不是 CAN 枚举。'
'任务号、锁定、EBS、无人状态的数值范围始终校验,有效标志为 0 时也不能发送越界值。', '',
'发送顺序:AMI 确认按钮 -> UART 任务帧 -> MCU CAN `0x300` 请求 -> VCU 裁决 -> '
'VCU 周期 CAN `0x183` 实际状态 -> AMI 显示。确认发送后可继续显示此前实际任务,'
'直到 VCU 更新或反馈超时。', '',
'发送工具预览和实发示例:', '', '```powershell',
'python scripts/send_vcu_status_can.py --task 2 --locked 1 --ebs-normal 1 --dry-run',
'python scripts/send_vcu_status_can.py --interface pcan --channel PCAN_USBBUS1 --bitrate 500000 --task 2 --locked 1 --ebs-normal 1 --duration 10',
'python scripts/send_vcu_status_can.py --task 2 --locked 1 --ebs-normal 1 --as-state 1 --dry-run',
'python scripts/send_vcu_status_can.py --interface pcan --channel PCAN_USBBUS1 --bitrate 500000 --task 2 --locked 1 --ebs-normal 1 --as-state 1 --duration 10',
'# CAN2 改为 --bitrate 250000;Windows 适配器需要时添加 --dll-dir 指定 DLL 目录',
'```', '',
'屏内全局变量:`current_task=0..6`(7 未知)、`lock_state=0/1/2`、`ebs_state=0/1/2`'
'(0 未知、1 绿、2 红)。MCU 更新这些变量,dash 后初始化和 100 ms 定时器更新文案、颜色和任务按钮;切页保留反馈。', '',
'(0 未知、1 绿、2 红);`as_state=0..5`,6 未知。MCU 更新这些变量,dash 后初始化和 100 ms 定时器更新文案、颜色和任务按钮;切页保留反馈。', '',
'## AMI 任务请求语义', '',
'| Task_ID | 任务 |', '| --- | --- |',
'| 1 | 八字绕环 |', '| 2 | 直线加速 |', '| 3 | 高速循迹 |',
@@ -188,6 +210,7 @@ def render():
'当前实际任务由 `0x183` 反馈,不按请求立即显示;未定义逐请求 ACK 或运行完成报文,也不把任务号换成 ASSI 灯状态。', '',
'## 显示缩放与失效规则', '',
'| 显示项 | 输入和显示规则 | 失效条件 |', '| --- | --- | --- |',
'| 车速 | `0x6A2 / DLC 4` 双前轮 uint16 mm/s 平均,`((左+右)*9+2500)/5000`,整数 km/h | 500 ms 无有效轮速帧;保留数字变灰 |',
'| 低压电池 | `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 状态 |',
@@ -205,17 +228,17 @@ def render():
'全部低电平正常、高电平故障,d15 无来源保持未知。心跳维持已有采样,'
'用户最新纠正 PD1 / Safe_S 为左电机互锁(d5),屏幕已删除 BSPD/TSMS。'
'心跳和 GPIO 均停止超过 500 ms 后全部未知,恢复需要新的 GPIO 采样;'
'轮频 Hz 尚无周长和每圈脉冲数,不能直接当 km/h。', '',
'当前 0x6A6 每 1000 ms 上报一次,变化掩码累积期间变化;心跳继续维持已有采样。', '',
'有效值显示白色;未知、无效、超时显示深灰色,历史数值保留但不作为有效测量。'
'各源独立超时,其他流量不续期。每 5 秒重新同步界面,刚连接需等待首轮同步。', '',
'## 报文样例', '',
'| ID / DLC | 数据(十六进制) | 解码及仪表显示 |', '| --- | --- | --- |',
'| `0x300 / 8` | `01 02 00 01 E8 03 00 00` | 直线加速请求、序号 0、来源 AMI、上电 1000 ms |',
'| `0x183 / 8` | `02 01 01 07 00 00 00 01` | 当前直线加速、已锁定、EBS 正常,三项有效 |',
'| `0x183 / 8` | `02 01 01 0F 00 01 00 02` | 当前直线加速、已锁定、AS Ready、EBS 正常,四项有效 |',
'| `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 |',
'| `0x6A2 / 4` | `10 27 F8 2A` | 左前 10.000 / 右前 11.000 m/s;平均 37.8 km/h,显示 38 |',
'| `0x140 / 8` | `0F A0 14 03 00 00 00 05` | 400.0 V / +12.3 A;显示 400 / 12 |',
'| `0x186040F3 / 8` 扩展 | `10 73 10 D1 00 00 00 05` | 421.1 V / -69.5 A;显示 421 / -70 |',
'| `0x156 / 8` | `42 FF 43 47 37 0A 50 41` | 第二路无效,有效最高温度 40 ℃ |',
@@ -233,6 +256,8 @@ def render():
'六个任务号与计数边界编码、大端偏置电流、无效温度枚举、全负温度和截短 DLC。'
'已安装并验证 cantools 44.1.0,接口参考 [cantools 官方文档](https://cantools.readthedocs.io/en/latest/)。', '',
'上述是静态及软件编解码验证,另校验两份 DBC 的速率、相同信号布局及源码 / CubeMX 位时序一致。'
'2026-10-07 v2.7 已完成 MCU 烧录及 16672 字节回读一致;真实 CAN1 / MCU / COM6 到大小屏官方模拟器的无人状态与触摸回传验收通过,'
'见 [无人状态报告](无人状态显示验证报告.md) 与 [小屏报告](小屏无人状态验证报告.md)。'
'v2.2 的烧录回读、新帧 CAN2 测试及串口显示核验状态见 [显示 CAN 测试报告](AMI自定义显示CAN测试报告.md)。'
'v2.1 已完成烧录、完整 Flash 回读比对、CAN1 500K 与 CAN2 250K 到官方 HMI 调试器的'
'实物测试,详见 [双 CAN 测试报告](CAN1_CAN2_v2.1测试报告.md)。'
+2 -2
View File
@@ -1,8 +1,8 @@
//以下代码只在上电时运行一次,一般用于全局变量定义和上电初始化数据
int sys0=0,sys1=0,sys2=0 //全局变量定义目前仅支持4字节有符号整形(int),不支持其他类型的全局变量声明,如需使用字符串类型可以在页面中使用变量控件来实现
int pending_task=0,current_task=7
int lock_state=0,ebs_state=0
int shown_task=-1,shown_lock=-1,shown_ebs=-1
int lock_state=0,ebs_state=0,as_state=6
int shown_task=-1,shown_lock=-1,shown_ebs=-1,shown_as=-1
int d0=0,d1=0,d2=0,d3=0,d4=0,d5=0,d6=0,d7=0,d8=0,d9=0,d10=0,d11=0,d12=0,d13=0,d14=0,d15=0
baud=9600//配置波特率
dim=100//配置屏幕背光
@@ -0,0 +1,142 @@
draw 0,0,799,479,10565
line 180,30,180,456,10565
line 524,30,524,456,10565
line 210,96,484,96,2047
shown_task=-1
shown_lock=-1
shown_ebs=-1
shown_as=-1
if(current_task!=shown_task)
{
rw.txt="未知"
rw.pco=33840
if(current_task==0)
{
rw.txt="无"
rw.pco=63488
}
if(current_task>=1&&current_task<=6)
{
rw.pco=2016
}
if(current_task==1)
{
rw.txt="八字绕环"
}
if(current_task==2)
{
rw.txt="直线加速"
}
if(current_task==3)
{
rw.txt="高速循迹"
}
if(current_task==4)
{
rw.txt="车检测试"
}
if(current_task==5)
{
rw.txt="EBS测试"
}
if(current_task==6)
{
rw.txt="手动驾驶"
}
bt0.val=0
if(current_task==1)
{
bt0.val=1
}
bt1.val=0
if(current_task==2)
{
bt1.val=1
}
bt2.val=0
if(current_task==3)
{
bt2.val=1
}
bt3.val=0
if(current_task==4)
{
bt3.val=1
}
bt4.val=0
if(current_task==5)
{
bt4.val=1
}
bt5.val=0
if(current_task==6)
{
bt5.val=1
}
shown_task=current_task
}
if(lock_state!=shown_lock)
{
sd.txt="锁定未知"
sd.pco=33840
if(lock_state==1)
{
sd.txt="已锁定"
sd.pco=2016
}
if(lock_state==2)
{
sd.txt="已解锁"
sd.pco=63488
}
shown_lock=lock_state
}
if(ebs_state!=shown_ebs)
{
EBS.txt="EBS 未知"
EBS.pco=33840
EBS.bco=0
if(ebs_state==1)
{
EBS.txt="EBS 正常"
EBS.pco=65535
EBS.bco=2016
}
if(ebs_state==2)
{
EBS.txt="EBS 故障"
EBS.pco=65535
EBS.bco=63488
}
shown_ebs=ebs_state
}
if(as_state!=shown_as)
{
xstr 330,379,174,53,4,33840,0,1,1,1,"无人未知"
if(as_state==0)
{
xstr 330,379,174,53,4,65535,0,1,1,1,"AS OFF"
}
if(as_state==1)
{
xstr 330,379,174,53,4,65504,0,1,1,1,"AS Ready"
}
if(as_state==2)
{
xstr 330,379,174,53,4,65504,0,1,1,1,"AS Driving"
}
if(as_state==3)
{
xstr 330,379,174,53,4,31,0,1,1,1,"AS Finished"
}
if(as_state==4)
{
xstr 330,379,174,53,4,63488,0,1,1,1,"AS Emergency"
}
if(as_state==5)
{
xstr 330,379,174,53,4,2016,0,1,1,1,"Manual Driving"
}
shown_as=as_state
}
@@ -0,0 +1,133 @@
if(current_task!=shown_task)
{
rw.txt="未知"
rw.pco=33840
if(current_task==0)
{
rw.txt="无"
rw.pco=63488
}
if(current_task>=1&&current_task<=6)
{
rw.pco=2016
}
if(current_task==1)
{
rw.txt="八字绕环"
}
if(current_task==2)
{
rw.txt="直线加速"
}
if(current_task==3)
{
rw.txt="高速循迹"
}
if(current_task==4)
{
rw.txt="车检测试"
}
if(current_task==5)
{
rw.txt="EBS测试"
}
if(current_task==6)
{
rw.txt="手动驾驶"
}
bt0.val=0
if(current_task==1)
{
bt0.val=1
}
bt1.val=0
if(current_task==2)
{
bt1.val=1
}
bt2.val=0
if(current_task==3)
{
bt2.val=1
}
bt3.val=0
if(current_task==4)
{
bt3.val=1
}
bt4.val=0
if(current_task==5)
{
bt4.val=1
}
bt5.val=0
if(current_task==6)
{
bt5.val=1
}
shown_task=current_task
}
if(lock_state!=shown_lock)
{
sd.txt="锁定未知"
sd.pco=33840
if(lock_state==1)
{
sd.txt="已锁定"
sd.pco=2016
}
if(lock_state==2)
{
sd.txt="已解锁"
sd.pco=63488
}
shown_lock=lock_state
}
if(ebs_state!=shown_ebs)
{
EBS.txt="EBS 未知"
EBS.pco=33840
EBS.bco=0
if(ebs_state==1)
{
EBS.txt="EBS 正常"
EBS.pco=65535
EBS.bco=2016
}
if(ebs_state==2)
{
EBS.txt="EBS 故障"
EBS.pco=65535
EBS.bco=63488
}
shown_ebs=ebs_state
}
if(as_state!=shown_as)
{
xstr 330,379,174,53,4,33840,0,1,1,1,"无人未知"
if(as_state==0)
{
xstr 330,379,174,53,4,65535,0,1,1,1,"AS OFF"
}
if(as_state==1)
{
xstr 330,379,174,53,4,65504,0,1,1,1,"AS Ready"
}
if(as_state==2)
{
xstr 330,379,174,53,4,65504,0,1,1,1,"AS Driving"
}
if(as_state==3)
{
xstr 330,379,174,53,4,31,0,1,1,1,"AS Finished"
}
if(as_state==4)
{
xstr 330,379,174,53,4,63488,0,1,1,1,"AS Emergency"
}
if(as_state==5)
{
xstr 330,379,174,53,4,2016,0,1,1,1,"Manual Driving"
}
shown_as=as_state
}
@@ -0,0 +1,316 @@
xstr 13,4,267,42,2,2047,0,1,1,1,"DEBUG 安全回路"
xstr 283,4,392,42,2,33840,0,1,1,1,"灰未知 绿正常 红故障"
draw 13,60,185,139,10565
xstr 15,64,170,32,4,65535,0,1,1,1,"配电箱"
draw 213,60,385,139,10565
xstr 215,64,170,32,4,65535,0,1,1,1,"左电机互锁"
draw 413,60,585,139,10565
xstr 415,64,170,32,4,65535,0,1,1,1,"左急停"
draw 613,60,785,139,10565
xstr 615,64,170,32,4,65535,0,1,1,1,"电池箱"
draw 213,148,385,228,10565
xstr 215,152,170,32,4,65535,0,1,1,1,"回路末端"
draw 413,148,585,228,10565
xstr 415,152,170,32,4,65535,0,1,1,1,"驾舱/右急停"
draw 613,148,785,228,10565
xstr 615,152,170,32,4,65535,0,1,1,1,"行程/惯性"
line 187,99,213,99,2047
line 207,94,213,99,2047
line 207,104,213,99,2047
line 387,99,413,99,2047
line 407,94,413,99,2047
line 407,104,413,99,2047
line 587,99,613,99,2047
line 607,94,613,99,2047
line 607,104,613,99,2047
line 787,99,797,99,2047
line 797,99,797,187,2047
line 797,187,787,187,2047
line 587,187,613,187,2047
line 593,182,587,187,2047
line 593,192,587,187,2047
line 387,187,413,187,2047
line 393,182,387,187,2047
line 393,192,387,187,2047
draw 13,242,385,321,10565
xstr 15,245,370,34,4,50712,0,1,1,1,"RES"
draw 413,242,785,321,10565
xstr 415,245,370,34,4,50712,0,1,1,1,"主控盒"
draw 13,332,185,404,10565
xstr 15,335,170,34,4,2047,0,1,1,1,"EBS 舵机"
draw 213,332,385,404,10565
xstr 215,335,170,34,4,2047,0,1,1,1,"EBS 电磁阀"
draw 413,332,585,404,10565
xstr 415,335,170,34,4,2047,0,1,1,1,"EBS 气压"
draw 613,332,785,404,10565
xstr 615,335,170,34,4,2047,0,1,1,1,"EBS 油压"
draw 13,409,185,478,10565
xstr 15,413,170,34,4,2047,0,1,1,1,"无人驾驶惯导"
draw 213,409,385,478,10565
xstr 215,413,170,34,4,2047,0,1,1,1,"无人驾驶轮速"
draw 413,409,585,478,10565
xstr 415,413,170,34,4,2047,0,1,1,1,"无人驾驶相机"
draw 613,409,785,478,10565
xstr 615,413,170,34,4,2047,0,1,1,1,"主控盒12V"
if(d0==1)
{
t0.txt="正常"
t0.pco=2016
}else
{
if(d0==2)
{
t0.txt="故障"
t0.pco=63488
}else
{
t0.txt="未知"
t0.pco=33840
}
}
if(d1==1)
{
t1.txt="正常"
t1.pco=2016
t3.txt="正常"
t3.pco=2016
}else
{
if(d1==2)
{
t1.txt="故障"
t1.pco=63488
t3.txt="故障"
t3.pco=63488
}else
{
t1.txt="未知"
t1.pco=33840
t3.txt="未知"
t3.pco=33840
}
}
if(d2==1)
{
t2.txt="正常"
t2.pco=2016
}else
{
if(d2==2)
{
t2.txt="故障"
t2.pco=63488
}else
{
t2.txt="未知"
t2.pco=33840
}
}
if(d3==1)
{
t4.txt="正常"
t4.pco=2016
}else
{
if(d3==2)
{
t4.txt="故障"
t4.pco=63488
}else
{
t4.txt="未知"
t4.pco=33840
}
}
if(d4==1)
{
t5.txt="正常"
t5.pco=2016
}else
{
if(d4==2)
{
t5.txt="故障"
t5.pco=63488
}else
{
t5.txt="未知"
t5.pco=33840
}
}
if(d5==1)
{
t6.txt="正常"
t6.pco=2016
}else
{
if(d5==2)
{
t6.txt="故障"
t6.pco=63488
}else
{
t6.txt="未知"
t6.pco=33840
}
}
if(d6==1)
{
t7.txt="正常"
t7.pco=2016
}else
{
if(d6==2)
{
t7.txt="故障"
t7.pco=63488
}else
{
t7.txt="未知"
t7.pco=33840
}
}
if(d7==1)
{
t8.txt="正常"
t8.pco=2016
}else
{
if(d7==2)
{
t8.txt="故障"
t8.pco=63488
}else
{
t8.txt="未知"
t8.pco=33840
}
}
if(d8==1)
{
t9.txt="正常"
t9.pco=2016
}else
{
if(d8==2)
{
t9.txt="故障"
t9.pco=63488
}else
{
t9.txt="未知"
t9.pco=33840
}
}
if(d9==1)
{
t10.txt="正常"
t10.pco=2016
}else
{
if(d9==2)
{
t10.txt="故障"
t10.pco=63488
}else
{
t10.txt="未知"
t10.pco=33840
}
}
if(d10==1)
{
t11.txt="正常"
t11.pco=2016
}else
{
if(d10==2)
{
t11.txt="故障"
t11.pco=63488
}else
{
t11.txt="未知"
t11.pco=33840
}
}
if(d11==1)
{
t12.txt="正常"
t12.pco=2016
}else
{
if(d11==2)
{
t12.txt="故障"
t12.pco=63488
}else
{
t12.txt="未知"
t12.pco=33840
}
}
if(d12==1)
{
t13.txt="正常"
t13.pco=2016
}else
{
if(d12==2)
{
t13.txt="故障"
t13.pco=63488
}else
{
t13.txt="未知"
t13.pco=33840
}
}
if(d13==1)
{
t14.txt="正常"
t14.pco=2016
}else
{
if(d13==2)
{
t14.txt="故障"
t14.pco=63488
}else
{
t14.txt="未知"
t14.pco=33840
}
}
if(d14==1)
{
t15.txt="正常"
t15.pco=2016
}else
{
if(d14==2)
{
t15.txt="故障"
t15.pco=63488
}else
{
t15.txt="未知"
t15.pco=33840
}
}
if(d15==1)
{
t16.txt="正常"
t16.pco=2016
}else
{
if(d15==2)
{
t16.txt="故障"
t16.pco=63488
}else
{
t16.txt="未知"
t16.pco=33840
}
}
+30
View File
@@ -1,6 +1,7 @@
shown_task=-1
shown_lock=-1
shown_ebs=-1
shown_as=-1
if(current_task!=shown_task)
{
rw.txt="未知"
@@ -105,3 +106,32 @@ if(ebs_state!=shown_ebs)
}
shown_ebs=ebs_state
}
if(as_state!=shown_as)
{
xstr 194,215,108,30,0,33840,0,1,1,1,"无人未知"
if(as_state==0)
{
xstr 194,215,108,30,0,65535,0,1,1,1,"AS OFF"
}
if(as_state==1)
{
xstr 194,215,108,30,0,65504,0,1,1,1,"AS Ready"
}
if(as_state==2)
{
xstr 194,215,108,30,0,65504,0,1,1,1,"AS Driving"
}
if(as_state==3)
{
xstr 194,215,108,30,0,31,0,1,1,1,"AS Finished"
}
if(as_state==4)
{
xstr 194,215,108,30,0,63488,0,1,1,1,"AS Emergency"
}
if(as_state==5)
{
xstr 194,215,108,30,0,2016,0,1,1,1,"有人驾驶"
}
shown_as=as_state
}
+29
View File
@@ -102,3 +102,32 @@ if(ebs_state!=shown_ebs)
}
shown_ebs=ebs_state
}
if(as_state!=shown_as)
{
xstr 194,215,108,30,0,33840,0,1,1,1,"无人未知"
if(as_state==0)
{
xstr 194,215,108,30,0,65535,0,1,1,1,"AS OFF"
}
if(as_state==1)
{
xstr 194,215,108,30,0,65504,0,1,1,1,"AS Ready"
}
if(as_state==2)
{
xstr 194,215,108,30,0,65504,0,1,1,1,"AS Driving"
}
if(as_state==3)
{
xstr 194,215,108,30,0,31,0,1,1,1,"AS Finished"
}
if(as_state==4)
{
xstr 194,215,108,30,0,63488,0,1,1,1,"AS Emergency"
}
if(as_state==5)
{
xstr 194,215,108,30,0,2016,0,1,1,1,"有人驾驶"
}
shown_as=as_state
}
+2
View File
@@ -0,0 +1,2 @@
unicorn==2.1.4
pyelftools==0.33
+19 -7
View File
@@ -5,12 +5,16 @@ import os
import time
def build_payload(task, locked, ebs_normal, valid=7, sequence=0):
def build_payload(task, locked, ebs_normal, valid=7, sequence=0, as_state=0, version=2):
if task not in range(7) or locked not in (0, 1) or ebs_normal not in (0, 1):
raise ValueError('task must be 0..6; locked and ebs-normal must be 0 or 1')
if valid not in range(8) or sequence not in range(256):
raise ValueError('valid must be 0..7 and sequence must be 0..255')
return bytes([task, locked, ebs_normal, valid, sequence, 0, 0, 1])
if version not in (1, 2) or as_state not in range(6):
raise ValueError('version must be 1 or 2; as-state must be 0..5')
if valid not in range(8 if version == 1 else 16) or sequence not in range(256):
raise ValueError('valid must be 0..7 (v1) or 0..15 (v2); sequence must be 0..255')
if version == 1 and as_state != 0:
raise ValueError('v1 has no AS state; byte 5 must be zero')
return bytes([task, locked, ebs_normal, valid, sequence, as_state, 0, version])
def main():
@@ -18,7 +22,10 @@ def main():
parser.add_argument('--task', type=int, required=True, choices=range(7))
parser.add_argument('--locked', type=int, required=True, choices=(0, 1))
parser.add_argument('--ebs-normal', type=int, required=True, choices=(0, 1))
parser.add_argument('--valid', type=int, default=7)
parser.add_argument('--valid', type=int, help='default 15 with --as-state, otherwise 7')
parser.add_argument('--as-state', type=int, choices=range(6),
help='0 OFF, 1 Ready, 2 Driving, 3 Finished, 4 Emergency, 5 Manual')
parser.add_argument('--version', type=int, choices=(1, 2), default=2)
parser.add_argument('--sequence', type=int, default=0)
parser.add_argument('--interface', default='pcan')
parser.add_argument('--channel', default='PCAN_USBBUS1')
@@ -29,7 +36,12 @@ def main():
parser.add_argument('--dry-run', action='store_true')
args = parser.parse_args()
try:
payload = build_payload(args.task, args.locked, args.ebs_normal, args.valid, args.sequence)
if args.version == 1 and args.as_state is not None:
raise ValueError('--as-state requires --version 2')
valid = args.valid if args.valid is not None else (15 if args.as_state is not None else 7)
as_state = args.as_state if args.as_state is not None else 0
payload = build_payload(args.task, args.locked, args.ebs_normal, valid, args.sequence,
as_state, args.version)
except ValueError as exc:
parser.error(str(exc))
if not (0 < args.period <= .5 and 0 < args.duration and math.isfinite(args.duration)):
@@ -50,7 +62,7 @@ def main():
deadline = started
while time.monotonic() < started + args.duration:
payload = build_payload(args.task, args.locked, args.ebs_normal,
args.valid, (args.sequence + count) & 255)
valid, (args.sequence + count) & 255, as_state, args.version)
bus.send(can.Message(arbitration_id=0x183, is_extended_id=False, data=payload), timeout=.2)
count += 1
deadline += args.period
+86
View File
@@ -0,0 +1,86 @@
"""Verify actual ARM CAN -> AS/VCU display -> UART code with mocked HAL I/O."""
import argparse
import hashlib
import json
from pathlib import Path
from verify_wheel_speed_firmware import Firmware, ROOT
from send_vcu_status_can import build_payload
def verify(path):
checks=[]
f=Firmware(path)
def states():
return list(f.uc.mem_read(f.symbols['vcu_display_state'],4))
def flush():
f.uc.mem_write(f.symbols['vcu_display_dirty'],b'\x0f')
f.commands.clear()
f.drain()
expected=[f'{prefix}={value}' for prefix,value in zip(
('current_task','lock_state','ebs_state','as_state'),states())]
assert all(cmd in f.commands for cmd in expected),(expected,f.commands)
assert states()==[7,0,0,6]
flush()
checks.append('boot: all four unknown; complete UART commands end in real FF FF FF')
for port in (1,2):
for task in range(7):
for as_state in range(6):
for flags in range(16):
f.now=1000
f.receive(build_payload(task,task&1,(task+1)&1,flags,255,as_state),port,ident=0x183)
expected=[task if flags&1 else 7,
(1 if task&1 else 2) if flags&2 else 0,
(1 if (task+1)&1 else 2) if flags&4 else 0,
as_state if flags&8 else 6]
assert states()==expected,(port,task,as_state,flags,states())
flush()
checks.append(f'CAN{port}: all 7 tasks x 6 AS states x 16 independent validity combinations; UART checked')
for flags in range(8):
f.receive(build_payload(2,1,1,15,0,4),ident=0x183)
f.receive(build_payload(2,1,1,flags,0,version=1),ident=0x183)
assert states()[3]==6
flush()
checks.append('legacy v1 always clears previous v2 AS to unknown; all 8 legacy flags accepted')
baseline=build_payload(2,1,1,15,0,1)
malformed=[]
for index,value in [(0,7),(1,2),(2,2),(3,16),(5,6),(5,255),(6,1),(7,0),(7,3)]:
b=bytearray(baseline);b[index]=value;malformed.append((bytes(b),{}))
b=bytearray(baseline);b[3]=7;b[5]=6;malformed.append((bytes(b),{}))
for index,value in [(3,8),(5,1),(6,1)]:
b=bytearray(build_payload(2,1,1,7,0,version=1));b[index]=value;malformed.append((bytes(b),{}))
for opts in ({'dlc':7},{'dlc':9},{'id_type':0x40000000},{'remote':0x20000000},{'fd':0x00200000}):
malformed.append((baseline,opts))
for port in (1,2):
for data,opts in malformed:
f.now=2000;f.receive(baseline,port,ident=0x183)
before=states();f.now=2200;f.receive(data,port,ident=0x183,**opts)
assert states()==before
assert f.read_u32('vcu_status_last_rx_ms')==2000
f.service(2501);assert states()==[7,0,0,6]
checks.append('both ports: malformed DLC/version/enum/reserved/extended/remote/FD rejected without timeout renewal')
for start,boundary,expired in ((0,500,501),(0xFFFFFF00,244,245)):
f=Firmware(path)
f.now=start;f.receive(baseline,ident=0x183)
f.service(boundary);assert states()==[2,1,1,1]
f.service(expired);assert states()==[7,0,0,6]
if f.now < 800: f.now=800
flush()
checks.append('timeout at >500 ms, tick zero and wrap verified; all four unknown transmitted')
f=Firmware(path);f.now=1000;f.receive(baseline,ident=0x183)
f.now=1400;f.receive(bytes(4),ident=0x6A2)
f.service(1501);assert states()==[7,0,0,6]
f.now=1502;f.receive(baseline,ident=0x183);flush();assert states()==[2,1,1,1]
f.now=6502;f.receive(baseline,ident=0x183)
f.commands.clear();f.service();f.drain()
assert 'as_state=1' in f.commands
checks.append('other CAN cannot renew AS; same-value recovery and periodic resync UART work')
return dict(elf=str(path.relative_to(ROOT)),sha256=hashlib.sha256(path.read_bytes()).hexdigest(),
result='passed',checks=checks,evidence='ARM emulation with mocked HAL; no physical CAN/HMI')
if __name__=='__main__':
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--output',type=Path)
a=p.parse_args()
result=[verify(ROOT/f'firmware/build/{mode}/AMI_Controller.elf') for mode in ('Debug','Release')]
text=json.dumps(result,ensure_ascii=False,indent=2)
if a.output:a.output.parent.mkdir(parents=True,exist_ok=True);a.output.write_text(text+'\n',encoding='utf-8')
print(text)
+28 -7
View File
@@ -74,7 +74,7 @@ def main():
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 = {0x182:(False,8), 0x183:(False,8), 0x300:(False,8), 0x301:(False,8), 0x6A0:(False,8), 0x6A1:(False,2),
0x6A2:(False,8), 0x6A4:(False,4), 0x6A6:(False,8),
0x6A2:(False,4), 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}
@@ -90,12 +90,13 @@ def main():
assert m.senders == (['VCU'] if m.frame_id in (0x182,0x183) else
['SDC_ACQ_DAC'] if 0x6A0 <= m.frame_id <= 0x6A6 else ['BMS'])
assert db.dbc.attributes['Baudrate'].value == 500000
for ident, cycle in [(0x301,100), (0x182,100), (0x183,100), (0x6A0,100), (0x6A1,500), (0x6A2,20)]:
for ident, cycle in [(0x301,100), (0x182,100), (0x183,100), (0x6A0,100), (0x6A1,500), (0x6A2,20), (0x6A6,1000)]:
assert db.get_message_by_frame_id(ident).cycle_time == cycle
for ident in [0x300,0x6A4,0x6A6,0x140,0x156,0x186040F3,0x186140F3]:
for ident in [0x300,0x6A4,0x140,0x156,0x186040F3,0x186140F3]:
assert db.get_message_by_frame_id(ident).cycle_time in (None,0)
assert len(db2.messages) == 12
assert sum(len(m.signals) for m in db2.messages) == 49
assert all(m.frame_id != 0x301 for m in db2.messages)
heartbeat = db.get_message_by_frame_id(0x301)
for sequence, uptime in ((0,100), (65535,4294967295), (0,0)):
@@ -119,9 +120,17 @@ def main():
assert values['EBS_Normal']==((task_id+1)&1)
assert status.encode(values)==raw
assert db2.get_message_by_frame_id(0x183).encode(values)==raw
assert build_vcu_payload(task_id,task_id&1,(task_id+1)&1,flags,255)==raw
assert build_vcu_payload(task_id,task_id&1,(task_id+1)&1,flags,255,version=1)==raw
for as_state in range(6):
for flags in range(16):
raw = build_vcu_payload(2, 1, 1, flags, 255, as_state)
values = decode(0x183, raw.hex(' '))
assert values['AS_State'] == as_state and values['AS_Valid'] == ((flags >> 3) & 1)
assert values['Reserved'] == 0 and values['Protocol_Version'] == 2
assert status.encode(values) == raw
assert db2.get_message_by_frame_id(0x183).encode(values) == raw
for arguments in ((7,1,1,7,0), (2,2,1,7,0), (2,1,2,7,0),
(2,1,1,8,0), (2,1,1,7,256)):
(2,1,1,16,0), (2,1,1,7,256)):
try:
build_vcu_payload(*arguments)
except ValueError:
@@ -177,8 +186,20 @@ def main():
assert heartbeat['Board_Identifier']==0xA1 and heartbeat['Protocol_Version']==1
assert heartbeat['Heartbeat_Sequence']==0x1234
near(heartbeat['Uptime'],1.0)
freq=decode(0x6A2,'32 00 64 00 96 00 C8 00')
assert list(freq.values()) == [50,100,150,200]
wheel = db.get_message_by_frame_id(0x6A2)
assert wheel.name == 'SDC_WheelSpeed'
for raw, speeds in [('00 00 00 00',(0,0)), ('10 27 F8 2A',(10,11)),
('FF FF FF FF',(65.535,65.535)), ('00 00 FF FF',(0,65.535))]:
values = decode(0x6A2,raw)
for actual, expected_speed in zip(values.values(),speeds,strict=True):
near(actual,expected_speed)
assert wheel.encode(values) == bytes.fromhex(raw)
try:
wheel.decode(bytes.fromhex('32 00 64 00 96 00 C8 00'),allow_excess=False)
except cantools.database.DecodeError:
pass
else:
raise AssertionError('Old DLC 8 wheel-frequency payload accepted')
gpio=decode(0x6A6,'AA 55 FF 7F E8 03 00 00')
assert gpio['SDC_Input_State']==0x55AA and gpio['SDC_Changed_Mask']==0x7fff
near(gpio['Timestamp'],1.0)
+214
View File
@@ -0,0 +1,214 @@
"""Execute compiled STM32 firmware in Unicorn; HAL I/O is mocked, no hardware used.
Requires scripts/requirements-test.txt and a built Debug or Release ELF.
Tests CAN RX -> display state -> ASCII UART DMA commands using actual ARM code.
"""
import argparse
from decimal import Decimal, ROUND_HALF_UP
import hashlib
import json
from pathlib import Path
import struct
from elftools.elf.elffile import ELFFile
from unicorn import Uc, UC_ARCH_ARM, UC_MODE_THUMB, UC_MODE_MCLASS, UC_HOOK_CODE
from unicorn.arm_const import (
UC_ARM_REG_R0, UC_ARM_REG_R1, UC_ARM_REG_R2, UC_ARM_REG_R3,
UC_ARM_REG_SP, UC_ARM_REG_LR, UC_ARM_REG_PC, UC_ARM_REG_XPSR,
)
ROOT = Path(__file__).resolve().parents[1]
REGS = (UC_ARM_REG_R0, UC_ARM_REG_R1, UC_ARM_REG_R2, UC_ARM_REG_R3)
class Firmware:
def __init__(self, path):
self.uc = Uc(UC_ARCH_ARM, UC_MODE_THUMB | UC_MODE_MCLASS)
self.uc.mem_map(0x08000000, 0x20000)
self.uc.mem_map(0x20000000, 0x24000)
self.uc.mem_map(0x40000000, 0x30000)
self.uc.mem_map(0x50000000, 0x10000)
with path.open('rb') as file:
elf = ELFFile(file)
self.symbols = {s.name: s['st_value'] for s in elf.get_section_by_name('.symtab').iter_symbols()}
for segment in elf.iter_segments():
if segment['p_type'] == 'PT_LOAD' and segment['p_filesz']:
self.uc.mem_write(segment['p_vaddr'], segment.data())
self.now = 800
self.rx = []
self.commands = []
self.hooks = {self.symbols[name] & ~1: name for name in (
'HAL_GetTick', 'HAL_GPIO_WritePin', 'HAL_GPIO_TogglePin',
'HAL_FDCAN_GetRxFifoFillLevel', 'HAL_FDCAN_GetRxMessage',
'HAL_FDCAN_GetTxFifoFreeLevel', 'HAL_UART_Transmit_DMA',
)}
self.uc.hook_add(UC_HOOK_CODE, self.hook)
for port, instance in ((1,0x40006400), (2,0x40006800)):
self.write_u32(f'hfdcan{port}', instance)
def hook(self, uc, address, size, data):
name = self.hooks.get(address)
if name is None:
return
result = 0
if name == 'HAL_GetTick':
result = self.now
elif name == 'HAL_FDCAN_GetRxFifoFillLevel':
result = len(self.rx)
elif name == 'HAL_FDCAN_GetRxMessage':
header, payload = self.rx.pop(0)
uc.mem_write(uc.reg_read(UC_ARM_REG_R2), header)
uc.mem_write(uc.reg_read(UC_ARM_REG_R3), payload)
elif name == 'HAL_UART_Transmit_DMA':
command = bytes(uc.mem_read(uc.reg_read(UC_ARM_REG_R1), uc.reg_read(UC_ARM_REG_R2)))
assert command.endswith(b'\xff\xff\xff'), command
self.commands.append(command[:-3].decode('ascii'))
# TX free level is zero: heartbeat never touches hardware in this test.
uc.reg_write(UC_ARM_REG_R0, result)
uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR))
def call(self, name, *args):
for register, value in zip(REGS, args):
self.uc.reg_write(register, value)
self.uc.reg_write(UC_ARM_REG_SP, 0x20023000)
self.uc.reg_write(UC_ARM_REG_LR, 0x0801FF01)
self.uc.reg_write(UC_ARM_REG_XPSR, 1 << 24)
self.uc.emu_start(self.symbols[name] | 1, 0x0801FF00, count=200000)
assert self.uc.reg_read(UC_ARM_REG_PC) == 0x0801FF00, f'{name} exceeded instruction budget'
return self.uc.reg_read(UC_ARM_REG_R0)
def read_u32(self, name, offset=0):
return struct.unpack('<I',self.uc.mem_read(self.symbols[name]+offset,4))[0]
def write_u32(self, name, value):
self.uc.mem_write(self.symbols[name],struct.pack('<I',value))
def valid(self):
return bool(self.uc.mem_read(self.symbols['display_state']+32,1)[0] & 1)
def speed(self):
return self.read_u32('display_state')
def receive(self, payload, port=1, ident=0x6A2, id_type=0, remote=0, fd=0, dlc=None):
dlc = len(payload) if dlc is None else dlc
header = struct.pack('<10I',ident,id_type,remote,dlc,0,0,fd,0,0,0)
self.rx.append((header,payload))
self.call('HAL_FDCAN_RxFifo0Callback',self.symbols[f'hfdcan{port}'],1)
assert not self.rx
def service(self, now=None):
if now is not None:
self.now = now & 0xFFFFFFFF
self.call('AMI_App_Service')
# Model the completion of the UART DMA transfer, without real UART I/O.
self.uc.mem_write(self.symbols['screen_tx_busy'],b'\0')
def drain(self):
for _ in range(40):
self.service()
def verify(path):
cases = []
f = Firmware(path)
f.write_u32('display_dirty',0xFFFFFFFF)
f.drain()
assert not f.valid() and f.speed() == 0
assert 'dash.speed.val=0' in f.commands and 'dash.speed.pco=16904' in f.commands
cases.append('boot unknown speed: grey zero, UART terminator verified')
samples = [(0,0), (10000,11000), (11000,10000), (0,20000),
(1249,1250), (1250,1250), (1250,1251), (65535,65535), (0,65535)]
for port in (1,2):
for left,right in samples:
f.now = 1000
f.commands.clear()
f.receive(struct.pack('<HH',left,right),port=port)
expected = int(((Decimal(left)+Decimal(right))/2000*Decimal('3.6'))
.quantize(Decimal('1'),rounding=ROUND_HALF_UP))
assert f.valid() and f.speed() == expected,(port,left,right,f.speed(),expected)
raw = struct.unpack('<HH',f.uc.mem_read(f.symbols['sdc_wheel_speed_mm_s'],4))
assert raw == (left,right)
assert f.read_u32('can_port_last_rx_ms',4*(port-1)) == f.now
f.drain()
# An unchanged display value may legitimately produce no UART command.
assert f'dash.speed.val={expected}' in f.commands or not any(
c.startswith('dash.speed.val=') for c in f.commands)
cases.append(f'CAN{port}: {len(samples)} zero, asymmetric, rounding and saturation vectors')
for kwargs,payload in [
({},bytes.fromhex('32 00 64 00 96 00 C8 00')),
({},bytes(3)), ({},bytes(5)), ({'dlc':9,'fd':0x00200000},bytes(12)),
({'id_type':0x40000000},bytes(4)),
({'remote':0x20000000},bytes(4)), ({'fd':0x00200000},bytes(4)),
({'ident':0x180},bytes(8)),
]:
f = Firmware(path)
f.now = 1000
f.receive(struct.pack('<HH',10000,11000))
snapshot = bytes(f.uc.mem_read(f.symbols['sdc_wheel_speed_mm_s'],4))
f.now = 1400
f.receive(payload,**kwargs)
assert f.speed() == 38 and f.valid()
assert f.read_u32('wheel_speed_last_rx_ms') == 1000
assert bytes(f.uc.mem_read(f.symbols['sdc_wheel_speed_mm_s'],4)) == snapshot
f.service(1500)
assert f.valid(), '500 ms boundary must still be valid'
f.service(1501)
assert not f.valid() and f.speed() == 38
f.drain()
assert 'dash.speed.pco=16904' in f.commands
cases.append('8 invalid DLC/format/legacy frames reject without overwriting or refreshing expiry')
f = Firmware(path)
f.now = 1000
f.receive(struct.pack('<HH',10000,11000))
f.drain()
assert 'dash.speed.val=38' in f.commands and 'dash.speed.pco=65535' in f.commands
f.now = 1501
for ident,payload in [(0x6A0,bytes.fromhex('A1 01 00 00 00 00 00 00')),
(0x6A6,bytes(8)), (0x6A1,bytes.fromhex('0C 30')),
(0x140,bytes(8)), (0x183,bytes.fromhex('02 01 01 07 00 00 00 01'))]:
f.receive(payload,ident=ident)
f.service()
assert not f.valid() and f.speed() == 38
f.now = 1600
f.commands.clear()
f.receive(struct.pack('<HH',10000,11000),port=2)
f.drain()
assert f.valid() and 'dash.speed.val=38' in f.commands and 'dash.speed.pco=65535' in f.commands
f.receive(bytes(4))
f.drain()
assert f.valid() and f.speed() == 0 and 'dash.speed.val=0' in f.commands
cases.append('unrelated SDC/BMS/VCU traffic cannot renew; same-value recovery and white zero work')
for started,boundary,expired in ((0,500,501),(0xFFFFFF00,244,245)):
f = Firmware(path)
f.now = started
f.receive(struct.pack('<HH',10000,11000))
f.service(boundary)
assert f.valid()
f.service(expired)
assert not f.valid() and f.speed() == 38
cases.append('tick zero and uint32 wrap: independent timeout boundary works')
return {'elf':str(path.relative_to(ROOT)), 'sha256':hashlib.sha256(path.read_bytes()).hexdigest(),
'result':'passed','checks':cases,'evidence':'ARM emulation with mocked HAL, not physical CAN/HMI'}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--elf',type=Path,action='append')
parser.add_argument('--output',type=Path)
args = parser.parse_args()
paths = args.elf or [ROOT/'firmware/build/Debug/AMI_Controller.elf',
ROOT/'firmware/build/Release/AMI_Controller.elf']
results = [verify(p.resolve()) for p in paths]
text = json.dumps(results,ensure_ascii=False,indent=2)
if args.output:
args.output.parent.mkdir(parents=True,exist_ok=True)
args.output.write_text(text+'\n',encoding='utf-8')
print(text)
if __name__ == '__main__':
main()