是什么:代码规范(coding standard)是团队对"代码长什么样、什么能做什么不能做"的书面约定。它解决的不是"好看",而是三个真金白银的问题:
== 误用,"ISR 里不许调用非 FromISR API"(见 04-21 页)封死内核崩溃;| 视角 | 没有规范 | 有规范 |
|---|---|---|
| reviewer 的意见 | 一半在挑命名/格式/魔数,真正逻辑问题被淹没 | 格式机器检查,意见聚焦设计、边界、并发 |
| 三个月后的自己 | "这行是谁写的?为什么这么写?"(是自己写的) | 结构可预期,凭命名与注释即可恢复上下文 |
| 多人并行开发 | 合并冲突里全是格式 diff,没人敢 rebase | 格式化器统一,diff 只含真实改动 |
| 交接与开源 | 代码只有作者能维护 | 风格接近行业惯例,外部贡献者也能提 PR |
面试怎么问:"你的代码规范习惯是什么?为什么要有规范?"——干答"命名驼峰、缩进 4 格"只值及格分;高分答法是说出规范的三层价值(阅读成本 / 整类消灭 bug / review 聚焦逻辑)+ 举一个自己被规范救过的具体例子(例如"review 同事指出我没有检查 malloc 返回值,后来那条路径真的 OOM 了")。
怎么做:嵌入式 C 社区(FreeRTOS、Zephyr、各家 SDK)的主流约定基本一致 —— 一致性比具体选哪种更重要。常用映射表:
| 实体 | 推荐约定 | 示例 |
|---|---|---|
| 函数(公共) | 模块前缀 + snake_case | motor_set_torque()、encoder_get_angle() |
| 函数(模块内私有) | static + 同风格(或不加前缀) | static int16_t iir_step(...) |
| 类型/结构体 | snake_case + _t 后缀(或前缀) | typedef struct { ... } pid_t; |
| 全局变量(文件内) | static + g 前缀 | static pid_t g_vloop; |
| 宏/枚举常量 | 全大写 + 下划线 + 模块前缀 | #define MOTOR_PWM_FREQ_HZ 20000u |
| 局部变量 | snake_case,短而有意义 | iq_ref、theta_mech(别用 a、tmp2) |
头文件三原则:①头文件只放"接口" —— 函数声明、类型、公共宏;禁止在 .h 里定义变量或写函数体(inline 除外);②include guard 必须有,防止重复包含:
/* ============ motor.h —— 头文件模板(接口与实现分离) ============ */
#ifndef MOTOR_H /* include guard: 防止重复包含 */
#define MOTOR_H
#include <stdint.h> /* .h 只包含自己用到的头, 越少越好 */
#include <stdbool.h>
/* ---- 公共类型 ---- */
typedef struct {
float kp, ki; /* PI 增益 */
float integ; /* 积分累加器 */
float out_max; /* 输出限幅 */
} motor_pi_t;
/* ---- 公共函数(带参数方向注释) ---- */
void motor_init(float pwm_freq_hz);
/* @brief 设置转矩指令 @param iq_a 相电流指令[A] @return 0=ok, -1=非法值 */
int motor_set_torque(float iq_a);
float motor_get_angle_rad(void);
#endif /* MOTOR_H */
/* ============ motor.c —— 实现文件 ============ */
#include "motor.h"
/* 私有函数与变量全部 static: 作用域不出本文件, 不污染全局命名空间 */
static float s_pwm_freq;
static int16_t iir_step(int16_t x) { /* ... */ }
是什么:防御式编程(defensive programming)= 不信任任何输入:公共函数入口检查参数合法性、所有返回值要么检查要么显式声明"故意忽略"。两条实践:①入口检查用断言 + 错误码组合 —— 开发期 configASSERT(p) 快速暴露调用方 bug,量产期返回错误码让调用者兜底;②错误码统一约定(0 = 成功、负数 = 错误),全工程一致,禁止有时返回 -1 有时返回 bool 有时静默吞掉。特别注意动态内存与外设返回值:malloc/calloc 返回值必须判 NULL;HAL 函数返回的 HAL_StatusTypeDef 在关键路径(初始化)必须检查。
/* ============ 防御式示例: 公共 API 的标准骨架 ============ */
int motor_set_torque(float iq_a)
{
/* 1) 参数检查: 非法输入立即拒绝, 不带病运行 */
if (!(iq_a > -MAX_PHASE_CURRENT && iq_a < MAX_PHASE_CURRENT)) {
return -1; /* NaN 也会被这条条件拦住(比较为假) */
}
if (!s_motor_enabled) {
return -2; /* 状态检查: 未使能时拒绝指令 */
}
/* 2) 核心: 限幅后执行, 限幅值来自编译期常量而非魔法数 */
float iq_clamped = CLAMP(iq_a, -MAX_PHASE_CURRENT, MAX_PHASE_CURRENT);
foc_update_iq_ref(iq_clamped);
/* 3) 关键外设调用必须检查返回值 */
if (HAL_OK != foc_start_pwm_update()) {
log_error("pwm update fail"); /* 失败路径也要有明确出口 */
return -3;
}
return 0;
}
/* 调用方: 返回值要么检查, 要么注释说明"故意忽略" */
if (0 != motor_set_torque(cmd)) { safe_shutdown(); }
是什么:MISRA C 是汽车/工业界为"安全攸关 C 代码"制定的语言子集标准,2012 版含 143 条规则 + 16 条指令。机器人关节驱动器虽不必全量过认证,但以下高频条款能直接封死一类事故(摘要为便于记忆的工程转述,精确条文以 MISRA C:2012 原文为准):
if (f(p) && p->n++) 禁止)—— 短路求值不可依赖;是什么:volatile 告诉编译器"这个变量可能被程序流之外的因素改变,禁止缓存到寄存器、禁止跨访问合并/重排优化"。三个正确场景与一个常见误解:
volatile uint32_t *reg = (volatile uint32_t *)0x40020000; —— 读寄存器每次必须真的发生;volatile bool g_frame_ready; —— 不加 volatile,主循环 while (!g_frame_ready) 可能被优化成死循环;面试怎么问:"volatile 和原子性是什么关系?volatile 变量还需要加锁吗?"——答:volatile 只禁优化、不保原子,多字节数据仍需锁/队列;只有"单字长 + 单写多读"的标志位可仅靠 volatile + 明确注释。能再补一句"寄存器访问还依赖编译器的 strict 别名处理,标准做法是 __IO 类型"更佳。
是什么:PEP8(Python Enhancement Proposal 8)是 Python 官方风格指南 —— 别背,记住"工具会替你执行":ruff / flake8 查违规、black / ruff-format 自动格式化。人工只需记住高频几条:
| 条目 | 规则 | 示例 |
|---|---|---|
| 缩进 | 4 空格,禁 Tab | — |
| 行宽 | 代码 ≤ 79~88(团队可放宽到 100),文档注释 ≤ 72 | — |
| 命名 | 函数/变量 lower_snake_case;类 CapWords;常量 UPPER_CASE;私有前缀单下划线 | load_model()、JointController、MAX_TORQUE、_cache |
| 导入 | 顶部三组:标准库 / 三方 / 本项目,组间空行;禁 from x import * | 见下代码块 |
| 比较 | 用 is None / is not None,别 == None | — |
| 文档字符串 | 公共模块/类/函数写 docstring(一句话摘要 + 参数 + 返回) | Google/NumPy 风格二选一,全库统一 |
"""joint_control: 人形机器人关节上位控制库(项目入口模块示例)。"""
# ---- 导入三段式: 标准库 / 三方库 / 本项目, 组间空行 ----
import time
from dataclasses import dataclass
import numpy as np
from joint_control.transport import CanBus
@dataclass
class JointState:
"""单个关节的实时状态(单位 SI)。"""
position: float # rad
velocity: float # rad/s
torque: float # N·m
def plan_trajectory(q_start: np.ndarray,
q_goal: np.ndarray,
duration: float) -> np.ndarray:
"""五次多项式插值。
Args:
q_start: 起始关节角向量 [n_joint]
q_goal: 目标关节角向量 [n_joint]
duration: 时长(秒), 必须 > 0
Returns:
形状 [n_joint, n_step] 的轨迹矩阵。
Raises:
ValueError: duration 非正时抛出。
"""
if duration <= 0:
raise ValueError(f"duration must be positive, got {duration}")
... # 返回 np.ndarray: 类型注解让 IDE/mypy 提前抓住 shape/单位错误
# ============ 推荐项目目录结构(src 布局) ============
joint_robot/
├── pyproject.toml # 项目元数据 + 依赖声明(现代标准)
├── requirements.txt # 或 requirements 系文件, 版本必须 pin
├── README.md
├── src/
│ └── joint_control/
│ ├── __init__.py
│ ├── controller.py # 控制逻辑
│ ├── transport.py # CAN/串口 通信
│ └── utils.py
├── tests/ # pytest 测试, 与源码一一对应
│ ├── test_controller.py
│ └── test_transport.py
└── scripts/ # 一次性脚本别混进库代码
└── calibrate_zero.py
依赖 pin 版本:requirements.txt 里写精确版本(numpy==1.26.4)而不是开放范围(numpy 或 numpy>=1.20)—— 机器人上位机依赖 ROS2/PyTorch 等重依赖库,上游小版本更新就可能破坏 API;可复现环境是实验科学的基本要求:半年后回滚复现实验结果,依赖不一致会让你怀疑人生。进阶:用 pyproject.toml + uv/poetry 的 lock 文件管理,原理相同。
面试怎么问:"你的 Python 项目怎么组织?依赖怎么管理?"——答 src 布局 + tests 分离 + 依赖 pin/lock;能补"mypy 静态检查类型注解、ruff 查 lint"说明工具链意识。
是什么:Verilog 一半是硬件描述、一半是仿真脚本 —— 可综合子集是"能被综合器变成真实电路"的那部分,写 RTL 必须守住清单:
always @(posedge clk ...) + 非阻塞赋值 <=;always @(*) + 阻塞赋值 =,所有分支都要给输出赋值(否则推断出锁存器 —— "锁存器意外"是新手 RTL 报告里最常见的警告);#delay 延时、无限循环 while(无静态边界)、任务内事件等待、initial 里做功能逻辑(FPGA RAM 初始化等少数例外);16'd40 而非 40),比较两侧位宽一致,避免隐式截断。怎么做:一个设计里异步复位与同步复位只能二选一(工程主流:异步复位、同步释放 async reset / sync de-assert,兼顾可靠释放与起跑状态明确),模板全库统一:
/* ============ 统一模板: 异步复位、同步释放的时序逻辑 ============ */
module encoder_counter #(parameter integer CNT_W = 14) (
input wire clk, // 采样时钟
input wire rst_n, // 低有效异步复位(全库统一低有效 _n)
input wire cnt_en, // 计数使能
output logic [CNT_W-1:0] cnt_o // 输出寄存器: _o 后缀
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt_o <= {CNT_W{1'b0}}; // 复位值显式写出
else if (cnt_en)
cnt_o <= cnt_o + 1'b1; // 非阻塞: 时序逻辑唯一写法
end
endmodule
命名后缀约定(与 4.1 模板配套):组合逻辑驱动的信号加 _w(wire),寄存器输出加 _r 或 _o,低有效信号加 _n,跨时钟域信号加 _sync —— 一眼分清"这根线是组合毛刺还是打拍后的稳定值",review 时省一半脑力。
| 赋值 | 语义 | 适用场景 | 用错的后果 |
|---|---|---|---|
= 阻塞 | 顺序执行,本句完成才走下一句(像 C) | 组合逻辑 always @(*)、testbench 激励 | 用在时序逻辑:仿真与综合行为不一致,竞争冒险,门级对不上 |
<= 非阻塞 | 本时刻统一调度,所有更新在时钟沿"同时"生效 | 时序逻辑 always @(posedge clk)(触发器阵列的真实语义) | 用在组合逻辑:输出滞后一拍,组合环/仿真不收敛 |
口诀(Cummings 论文流传最广):"时序用非阻塞、组合用阻塞、同一个 always 块里不混用"。同一模块里既写时序又写组合时,拆成两个 always 块,各自的赋值风格不交叉。
面试怎么问:"阻塞与非阻塞赋值的区别?混用会发生什么?"——答:阻塞像软件顺序执行,非阻塞模拟触发器"时钟沿统一切换"的真实硬件行为;时序块里用阻塞会造成仿真与综合不一致(仿真先更新,综合出的触发器却同时切换),潜在竞争;补一句"所有触发器仿真模型都是 NBA update 区更新"是深度信号。
怎么做:主流约定(Conventional Commits 简化版):<type>: <一句话摘要>,type 常用 feat(新功能)/ fix(修 bug)/ refactor(重构不改行为)/ docs / test / chore(构建与杂项)。摘要 50 字内、祈使句("add" 而不是 "added")、说明"做了什么"而不是"怎么做的";复杂改动空一行后写正文,说清动机与副作用。示例:
fix: clamp iq reference before PWM update to prevent overcurrent
iq 指令未经限幅直接下发, 在急停恢复瞬间可达 1.8 倍额定电流。
本改动在 foc_update_iq_ref() 入口统一 CLAMP 到 ±MAX_PHASE_CURRENT,
并新增边界测试 test_set_torque_boundary。
Refs: #42
是什么:小团队够用的简化流:①main 永远可编译可测(CI 红了优先修);②任何改动从 main 拉出 feature/xxx 或 fix/xxx 短命分支;③完成后提 PR(合并请求),至少一人 review 通过 + CI 通过才合并;④合并后删分支。PR 的自我修养:标题即 commit 规范、描述写清"为什么改 + 怎么验证的"、控制体量 —— 500 行以上的 PR 大概率被敷衍通过,review 的有效性随体量指数下降,"小步快跑"比"憋大招"快得多。
原则:仓库只进"源",不进"产物与机器配置"。常见该忽略项:编译产物(build/、*.o、*.elf、*.bin)、IDE/编辑器配置(.vscode/ 视团队约定、.idea/)、Python 的 __pycache__/、.venv/、Verilog 仿真产物(work/、*.vcd、xsim.dir/)。两个血泪条款:①node_modules / 大模型权重 / 数据集绝不入库(用 LFS 或外部存储);②密钥、密码、板卡序列号等机密永不入库 —— 即使后续删除,Git 历史里仍在,必须用环境变量或密钥管理工具。
面试怎么问:"讲讲你们的 Git 工作流。PR 里 reviewer 主要看什么?"——答分支模型 + commit 规范 + "小 PR"原则;reviewer 视角:先看测试有没有覆盖新逻辑,再看边界条件与并发,最后才是实现细节(参考第 6 节清单)。
import *误区:把某本书/某大厂的规范当圣经逐字执行,与团队现状冲突就硬掰。正解:规范的最高准则是一致性 —— 一个 80 分但全队统一的标准,胜过 100 分但每人一个版本的标准;引入新规则要走团队讨论 + 落进自动化检查(否则三天就会漂移)。与既有代码库共存时"进新代码先达标,旧代码顺手改、不专门翻新"是务实的折中。
误区:i++; /* i 加 1 */ 这类注释是噪音,而真正该写的"为什么"却缺失。正解:代码表达"做什么",注释解释"为什么这么做、为什么不用更显然的方案、单位与量纲、约束来源"。高价值注释示例:/* PWM 中心对齐点采样: 开关沿已结束, 电流纹波最小 —— 见 04-19 页 3.2 节 */。删除代码时同步删注释;过时注释比没注释更害人。
误区:if (v > 24) ... —— 24 是什么?温度上限?电压?写的人三个月后也答不上来。正解:数值常量一律具名(#define OVERTEMP_LIMIT_C 24 或 constexpr/枚举),名字里带物理量与单位;同一魔数出现两处时尤其危险 —— 只改一处、漏掉另一处是最隐蔽的 bug。特例:0、1、-1 等结构常量可不命名,数组下标 0 不需要宏。
| 误区 | 危害 | 正解 |
|---|---|---|
| 规范当教条逐字执行 | 团队内耗、效率下降 | 一致性优先;规则进自动化检查而非口头约定 |
| 注释复述代码(what) | 噪音 + 过时注释误导 | 注释解释 why、单位、约束来源 |
| 魔数散落代码各处 | 改一漏一、不可维护 | 具名常量带单位;同值两处必须同名同源 |
面试怎么问:"你觉得公司某条规范不合理,你会怎么做?"——考察协作成熟度:先理解规则背后的动机(多数规则都有事故史),仍有异议则带着具体案例在团队渠道提议修订,而不是悄悄违反或阳奉阴违。