SKILL.md 元数据
---
name: keil-armcc-build-analyzer
description: >-
[Scope: global] Keil MDK-ARM (ARMCC 5 / ARMClang 6) 命令行无头构建、.build_log.htm 警告分类审查与 .map 内存占用(Code/RO/RW/ZI 及栈水位)自动对比分析。
当用户需要编译 .uvprojx 工程、排查 ARMCC 5 编译/链接警告(如 UTF-8/GBK 编码告警、C89/C99 混用)、或审计每次代码修改前后的 Flash/RAM 字节增量时使用此技能。
---
1. 适用范围
- 技术方向:
common - 使用范围 (Scope):
global(适用于所有使用 Keil MDK-ARM.uvprojx/.uvproj的 MCU 工程) - 适用工具链:Keil μVision (
UV4.exe),ARM Compiler 5 (armccV5.06) 与 ARM Compiler 6 (armclang)。
2. 设计要求
- 绝不盲目宣称“编译通过”或“零警告”:
UV4.exe -b或-r的进程退出码(Exit Code):0表示 0 Error / 0 Warning,1表示仅存在 Warning(无 Error),>= 2表示存在 Error。- 警告处理:退出码为
1时虽然生成了.axf/.hex,但必须逐行解析.build_log.htm区分本次新增警告与历史遗留警告(如历史 GBK/UTF-8 特殊字符警告),严禁新增任何警告,也严禁在存在历史警告时笼统汇报“零警告”。
- 强制量化每次改动的 Flash / RAM 增量 (
Code / RO / RW / ZI):- 在嵌入式裸机工程中新增缓冲、字库、状态表后,必须从
.build_log.htm或.map提取四项核心指标并计算增量(Δ):- Flash 占用 =
Code + RO-data + RW-data - SRAM 静态占用 =
RW-data + ZI-data
- Flash 占用 =
- 在嵌入式裸机工程中新增缓冲、字库、状态表后,必须从
- 保护
.uvprojx与编译器基线:- 未经用户明确授权,绝不将 ARMCC 5 (V5.06) 工程切换为 ARMClang 6,不修改优化等级(如
-O1)、芯片 Pack 版本或分散加载文件(.sct)。
- 未经用户明确授权,绝不将 ARMCC 5 (V5.06) 工程切换为 ARMClang 6,不修改优化等级(如
可直接运行的构建日志与
.map对比脚本见 scripts/analyze_keil_build.ps1(Windows 免依赖首选)与 scripts/analyze_keil_build.py;ARMCC 5 高频警告修复与.map符号审计指南见 references/armcc5-common-warnings-and-map.md。
3. 操作步骤
- 修改前采集基线(Baseline Snapshot):
- 修改代码前先运行分析脚本读取现有
Objects/*.build_log.htm与Listings/*.map(或Objects/*.map),保存基线 JSON:powershell -NoProfile -ExecutionPolicy Bypass -File "<skill-dir>/scripts/analyze_keil_build.ps1" -LogPath "N32G455/MDK-ARM/Objects/N32G455.build_log.htm" -SaveBaseline "baseline.json"
- 修改代码前先运行分析脚本读取现有
- 执行 Keil 命令行无头构建(Headless Build):
- 优先增量构建
-b(修改头文件或编译宏时使用全量重构-r),指定-t <TargetName>与-j0 -o build_cmd.log:& "C:\Keil_v5\UV4\UV4.exe" -b "N32G455\MDK-ARM\N32G455.uvprojx" -t "N32G455" -j0 -o "build_output.log" - 若当前环境未安装
UV4.exe,必须明确向用户报告“当前环境无法运行 Keil 构建,仅完成静态检查”,绝不伪造编译结果。
- 优先增量构建
- 解析构建日志与对比资源增量:
- 运行
scripts/analyze_keil_build.ps1对比修改前后的Code / RO-data / RW-data / ZI-data及警告列表:powershell -NoProfile -ExecutionPolicy Bypass -File "<skill-dir>/scripts/analyze_keil_build.ps1" -LogPath "N32G455/MDK-ARM/Objects/N32G455.build_log.htm" -MapPath "N32G455/MDK-ARM/Listings/N32G455.map" -CompareBaseline "baseline.json"
- 运行
- 清理构建副产物(Git 保护):
- 确认
Objects/、Listings/、*.build_log.htm、*.dep、*.uvguix.*均未被git add暂存。
- 确认
4. 关键安全与工具链检查清单
- 只读常量进 Flash (
RO-data):新增字库、状态转移表、菜单配置表、CRC 查表必须加const,并在.map中核对已落入RO-data而非RW-data。 - UTF-8 无 BOM 与 ARMCC 5 中文字符串:ARMCC 5 在未加
--locale=english或多字节字符跨界时易触发#870-D: invalid multibyte character sequence;对 UI 字面量优先使用\xHH十六进制 UTF-8 转义或统一通过app_i18n字典表管理。 - 栈空间 (
STACK) 与堆 (HEAP) 检查:在.map中检查STACK段的基址与大小,确保ZI-data增长后不会挤压栈空间,且裸机工程HEAP设为0(禁用malloc)。
5. 验证与交付汇报模板 (Verification Report)
每次交付代码时,按以下标准格式输出构建与资源证据:
- 编译器版本:
ARMCC V5.06 update 6 (build 750) - 编译结果:
0 Error(s), 0 New Warning(s)(如有历史警告,注明具体文件:行号与原因) - 资源占用与增量:
Code:127,928 B (+1,796 B)RO-data:34,272 B (+968 B)RW-data:304 B (+0 B)ZI-data:28,776 B (+16 B)-
总 Flash: 162,504 B总静态 RAM: 29,080 B
参考文档:ARMCC 5 高频警告排查与 .map 检查指南 (references/armcc5-common-warnings-and-map.md)
1. ARMCC V5.06 高频编译警告与标准修复法
| 警告编号与信息 | 触发原因 | 规范修复方式(零副作用) |
|---|---|---|
#870-D: invalid multibyte character sequence |
源码为 UTF-8(无 BOM)且注释或字符串中含有多字节中文字符(如 ≈、繁体字或奇数半角/全角边界),被 ARMCC 5 按系统默认 ANSI/GBK 代码页解析 |
1. C 字符串字面量改用十六进制字节转义(如 "\xE5\xAD\x98\xE5\x82\xA8");2. 注释中避免生僻符号(如将 ≈ 改为 ~ 或 约);3. 未经授权不要批量把文件转码为 GBK。 |
#177-D: variable "xxx" was declared but never referenced / function was declared but never referenced |
条件编译宏(如 APP_RECORD_STORE_VALIDATION_ENABLE == 0)关闭后,局部静态辅助函数或变量未参与调用 |
将仅在验证宏开启时使用的 static 函数放入同一 #if ... #endif 块内,或对未使用的接口形参显式写 (void)param;。 |
#188-D: enumerated type mixed with another type |
将 uint8_t 整数直接赋值给枚举类型变量,或不同枚举间隐式混用 |
先校验 uint8_t 范围合法,再使用显式类型转换 (MyEnumType)rawVal。 |
#68-D: integer conversion resulted in a change of sign |
无符号宏常量与有符号整数混算或将 -1 赋给无符号类型 |
明确无符号常量后缀 U / UL,有符号与无符号比较前先做范围确认与显式转换。 |
#223-D: function "xxx" declared implicitly |
调用了未包含头文件的函数(C99 下极易导致返回值截断或入参压栈错误) | 必须在对应模块 .h 中声明公共原型并 #include,严禁在 .c 内部手写外部 extern 声明。 |
2. Keil .map 文件的四大关键审查段落
打开 Listings/<Target>.map(或 Objects/<Target>.map),按顺序核查以下 4 个核心区域:
Section Cross References(模块交叉引用表):- 检查是否违反
APP -> DRV单向依赖原则(若发现drv_xxx.o引用了app_xxx.o的符号,说明驱动层反向耦合了业务层)。
- 检查是否违反
Removing Unused input sections from the image(死代码消除表):- 确认开启
--split_sections后未使用的库函数已被 Linker 裁剪;若新增的业务函数意外出现在此列表,说明漏接了主循环或表驱动入口。
- 确认开启
Memory Map of the image(内存地址排布表):ER_IROM1 (0x08000000):确认固件末尾地址未侵入片内 Flash 尾部参数扇区(如双槽 EEPROM 替代方案的尾页)或 Bootloader 区。RW_IRAM1 (0x20000000):- 定位最大的几个
ZERO(ZI) 段(如s_stripBuffer、传感器环形队列、页缓存); - 定位
STACK段起始地址与顶地址(Cortex-M 满递减栈从STACK顶部向下生长),确认栈大小(如0x00001000 = 4 KiB)充足。
- 定位最大的几个
Image Component Sizes(组件体积汇总表):- 逐个
.o目标文件列出Code | RO Data | RW Data | ZI Data,快速抓出哪一个模块吃掉了最多的 SRAM 或 Flash。
- 逐个
配套脚本:analyze_keil_build.ps1 (PowerShell 免依赖版)
# analyze_keil_build.ps1
# Zero-dependency PowerShell script to parse Keil MDK (.build_log.htm) and .map files.
param(
[Parameter(Mandatory = $true)]
[string]$LogPath,
[string]$MapPath = "",
[string]$SaveBaseline = "",
[string]$CompareBaseline = "",
[int]$Top = 8
)
$ErrorActionPreference = "Stop"
if (-not (Test-Path $LogPath)) {
Write-Error "Build log not found: $LogPath"
exit 2
}
$raw = Get-Content -Path $LogPath -Raw -Encoding UTF8
$clean = $raw -replace '<[^>]+>', ''
$clean = [System.Net.WebUtility]::HtmlDecode($clean)
$lines = $clean -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" }
$compiler = "Unknown"
$errors = $null
$warnings = $null
$size = $null
$diagnostics = @()
foreach ($line in $lines) {
if ($line -match 'V5\.06|ARMClang|ArmCC') {
$compiler = $line
}
if ($line -match 'Program Size:\s*Code=(\d+)\s+RO-data=(\d+)\s+RW-data=(\d+)\s+ZI-data=(\d+)') {
$code = [int]$Matches[1]
$ro = [int]$Matches[2]
$rw = [int]$Matches[3]
$zi = [int]$Matches[4]
$size = [ordered]@{
code = $code
ro_data = $ro
rw_data = $rw
zi_data = $zi
total_flash = $code + $ro + $rw
total_ram = $rw + $zi
}
}
if ($line -match '"[^"]+"\s*-\s*(\d+)\s*Error\(s\),\s*(\d+)\s*Warning\(s\)\.') {
$errors = [int]$Matches[1]
$warnings = [int]$Matches[2]
}
if ($line -match '^(.+?)\((\d+)\):\s*(warning|error):\s*(#[0-9A-Za-z-]+:.*)') {
$diagnostics += [PSCustomObject]@{
file = $Matches[1]
line = [int]$Matches[2]
severity = $Matches[3].ToUpper()
message = $Matches[4]
}
}
}
$result = [ordered]@{
log_path = (Resolve-Path $LogPath).Path
compiler = $compiler
errors = $errors
warnings = $warnings
size = $size
diagnostics = $diagnostics
}
if ($SaveBaseline -ne "") {
$result | ConvertTo-Json -Depth 5 | Set-Content -Path $SaveBaseline -Encoding UTF8
Write-Host "Saved baseline to $SaveBaseline" -ForegroundColor Cyan
}
$baseSize = $null
if ($CompareBaseline -ne "" -and (Test-Path $CompareBaseline)) {
$baseObj = Get-Content -Path $CompareBaseline -Raw -Encoding UTF8 | ConvertFrom-Json
$baseSize = $baseObj.size
}
function Format-Delta([int]$curr, $baseVal) {
if ($null -eq $baseVal) { return ("{0:N0} B" -f $curr) }
$diff = $curr - [int]$baseVal
$sign = if ($diff -ge 0) { "+" } else { "" }
return ("{0:N0} B ({1}{2:N0} B)" -f $curr, $sign, $diff)
}
Write-Host "=== Keil MDK Build Summary ===" -ForegroundColor Green
Write-Host ("Compiler : {0}" -f $compiler)
Write-Host ("Result : {0} Error(s), {1} Warning(s)" -f $errors, $warnings)
if ($null -ne $size) {
Write-Host "`n=== Memory Footprint (Program Size) ===" -ForegroundColor Green
Write-Host (" Code : {0}" -f (Format-Delta $size.code ($baseSize.code)))
Write-Host (" RO-data : {0}" -f (Format-Delta $size.ro_data ($baseSize.ro_data)))
Write-Host (" RW-data : {0}" -f (Format-Delta $size.rw_data ($baseSize.rw_data)))
Write-Host (" ZI-data : {0}" -f (Format-Delta $size.zi_data ($baseSize.zi_data)))
Write-Host (" Total Flash : {0}" -f (Format-Delta $size.total_flash ($baseSize.total_flash)))
Write-Host (" Total RAM : {0}" -f (Format-Delta $size.total_ram ($baseSize.total_ram)))
}
if ($diagnostics.Count -gt 0) {
Write-Host ("`n=== Diagnostics ({0}) ===" -f $diagnostics.Count) -ForegroundColor Yellow
foreach ($d in $diagnostics) {
Write-Host (" [{0}] {1}:{2} -> {3}" -f $d.severity, $d.file, $d.line, $d.message)
}
}
if ($MapPath -ne "" -and (Test-Path $MapPath)) {
$ramSyms = @()
Get-Content -Path $MapPath -Encoding UTF8 | ForEach-Object {
if ($_ -match '^\s*(0x[0-9a-fA-F]{8})\s+(0x[0-9a-fA-F]{8})\s+(Code|Data|Zero)\s+(RO|RW)\s+\d+\s+(\S+)\s+(\S+)') {
$szBytes = [Convert]::ToInt32($Matches[2], 16)
if ($szBytes -gt 0 -and $Matches[4] -eq "RW") {
$ramSyms += [PSCustomObject]@{
Size = $szBytes
Type = "$($Matches[3])/$($Matches[4])"
Section = $Matches[5]
Object = $Matches[6]
}
}
}
}
$topRam = $ramSyms | Sort-Object -Property Size -Descending | Select-Object -First $Top
if ($topRam.Count -gt 0) {
Write-Host ("`n=== Top {0} RAM (RW/ZI) Sections in .map ===" -f $Top) -ForegroundColor Green
foreach ($s in $topRam) {
Write-Host (" {0,8:N0} B | {1,-7} | {2} ({3})" -f $s.Size, $s.Type, $s.Section, $s.Object)
}
}
}
配套脚本:analyze_keil_build.py (Python 跨平台版)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
analyze_keil_build.py
Parse Keil MDK (.build_log.htm / build log) and .map file to extract:
1. Compiler version, Error count, Warning count & detailed warning list
2. Program Size (Code, RO-data, RW-data, ZI-data) and Flash/RAM totals
3. Delta comparison against a saved baseline JSON
4. Top largest RAM (ZI/RW) and Flash (Code/RO) symbols from the .map file
"""
import argparse
import html
import json
import os
import re
import sys
from typing import Dict, List, Optional, Any
RE_COMPILER = re.compile(r"Toolchain:\s*(.+)|Compiler:\s*(.+)|ARMCC\s+V[\d.]+.*")
RE_PROG_SIZE = re.compile(
r"Program Size:\s*Code=(\d+)\s+RO-data=(\d+)\s+RW-data=(\d+)\s+ZI-data=(\d+)"
)
RE_SUMMARY = re.compile(r'"[^"]+"\s*-\s*(\d+)\s*Error\(s\),\s*(\d+)\s*Warning\(s\)\.')
RE_DIAG_LINE = re.compile(
r"^(.+?)\((\d+)\):\s*(warning|error):\s*(#[0-9A-Za-z-]+:.*)", re.IGNORECASE
)
RE_MAP_SYM = re.compile(
r"^\s*(0x[0-9a-fA-F]{8})\s+(0x[0-9a-fA-F]{8})\s+(Code|Data|Zero)\s+(RO|RW)\s+\d+\s+(\S+)\s+(\S+)"
)
def strip_html(text: str) -> str:
text = re.sub(r"<[^>]+>", "", text)
return html.unescape(text)
def parse_build_log(log_path: str) -> Dict[str, Any]:
with open(log_path, "r", encoding="utf-8", errors="replace") as f:
raw = f.read()
clean = strip_html(raw) if log_path.lower().endswith((".htm", ".html")) else raw
lines = [line.strip() for line in clean.splitlines() if line.strip()]
result: Dict[str, Any] = {
"log_path": os.path.abspath(log_path),
"compiler": "Unknown",
"errors": None,
"warnings": None,
"size": None,
"diagnostics": [],
}
for line in lines:
if "V5.06" in line or "ARMClang" in line or "ArmCC" in line:
result["compiler"] = line
m_size = RE_PROG_SIZE.search(line)
if m_size:
code, ro, rw, zi = map(int, m_size.groups())
result["size"] = {
"code": code,
"ro_data": ro,
"rw_data": rw,
"zi_data": zi,
"total_flash": code + ro + rw,
"total_ram": rw + zi,
}
m_sum = RE_SUMMARY.search(line)
if m_sum:
result["errors"] = int(m_sum.group(1))
result["warnings"] = int(m_sum.group(2))
m_diag = RE_DIAG_LINE.match(line)
if m_diag:
result["diagnostics"].append(
{
"file": m_diag.group(1),
"line": int(m_diag.group(2)),
"severity": m_diag.group(3).lower(),
"message": m_diag.group(4),
}
)
return result
def parse_top_map_symbols(map_path: str, top_n: int = 10) -> Dict[str, List[Dict[str, Any]]]:
ram_syms: List[Dict[str, Any]] = []
flash_syms: List[Dict[str, Any]] = []
if not map_path or not os.path.isfile(map_path):
return {"top_ram": [], "top_flash": []}
with open(map_path, "r", encoding="utf-8", errors="replace") as f:
for line in f:
m = RE_MAP_SYM.match(line)
if not m:
continue
addr, size_hex, kind, attr, section, obj = m.groups()
size_bytes = int(size_hex, 16)
if size_bytes == 0:
continue
entry = {
"addr": addr,
"size": size_bytes,
"type": f"{kind}/{attr}",
"section": section,
"object": obj,
}
if attr == "RW":
ram_syms.append(entry)
else:
flash_syms.append(entry)
ram_syms.sort(key=lambda x: x["size"], reverse=True)
flash_syms.sort(key=lambda x: x["size"], reverse=True)
return {"top_ram": ram_syms[:top_n], "top_flash": flash_syms[:top_n]}
def fmt_delta(curr: int, base: Optional[int]) -> str:
if base is None:
return f"{curr:,} B"
diff = curr - base
sign = "+" if diff >= 0 else ""
return f"{curr:,} B ({sign}{diff:,} B)"
def main() -> int:
parser = argparse.ArgumentParser(description="Analyze Keil MDK build log and map file.")
parser.add_argument("--log", required=True, help="Path to .build_log.htm or build output log")
parser.add_argument("--map", default="", help="Optional path to .map file")
parser.add_argument("--save-baseline", default="", help="Save parsed metrics to JSON file")
parser.add_argument("--compare-baseline", default="", help="Compare against saved baseline JSON")
parser.add_argument("--top", type=int, default=8, help="Number of top symbols from .map")
args = parser.parse_args()
if not os.path.isfile(args.log):
print(f"ERROR: Build log not found: {args.log}", file=sys.stderr)
return 2
data = parse_build_log(args.log)
if args.save_baseline:
with open(args.save_baseline, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
print(f"Saved baseline to {args.save_baseline}")
base_size = None
if args.compare_baseline and os.path.isfile(args.compare_baseline):
with open(args.compare_baseline, "r", encoding="utf-8") as f:
base_data = json.load(f)
base_size = base_data.get("size")
print("=== Keil MDK Build Summary ===")
print(f"Compiler : {data['compiler']}")
print(f"Result : {data['errors']} Error(s), {data['warnings']} Warning(s)")
sz = data.get("size")
if sz:
print("\n=== Memory Footprint (Program Size) ===")
print(f" Code : {fmt_delta(sz['code'], base_size['code'] if base_size else None)}")
print(f" RO-data : {fmt_delta(sz['ro_data'], base_size['ro_data'] if base_size else None)}")
print(f" RW-data : {fmt_delta(sz['rw_data'], base_size['rw_data'] if base_size else None)}")
print(f" ZI-data : {fmt_delta(sz['zi_data'], base_size['zi_data'] if base_size else None)}")
print(f" Total Flash : {fmt_delta(sz['total_flash'], base_size['total_flash'] if base_size else None)}")
print(f" Total RAM : {fmt_delta(sz['total_ram'], base_size['total_ram'] if base_size else None)}")
if data["diagnostics"]:
print(f"\n=== Diagnostics ({len(data['diagnostics'])}) ===")
for d in data["diagnostics"]:
print(f" [{d['severity'].upper()}] {d['file']}:{d['line']} -> {d['message']}")
if args.map:
top_syms = parse_top_map_symbols(args.map, args.top)
if top_syms["top_ram"]:
print(f"\n=== Top {args.top} RAM (RW/ZI) Sections in .map ===")
for s in top_syms["top_ram"]:
print(f" {s['size']:>8,} B | {s['type']:<7} | {s['section']} ({s['object']})")
return 0 if (data["errors"] == 0) else 1
if __name__ == "__main__":
sys.exit(main())