骑行码表项目
骑行码表项目
为巽骑行码表项目面试深挖手册
用途:面试时从项目概括展开到源码、数据结构、RTOS、异常和取舍。文中“源码事实”来自当前工程;没有实测的帧率、功耗、吞吐和声学指标明确标成待测,不在面试中夸大。
1. 先把项目讲清楚
1.1 30 秒版本
这是一个基于 ESP32-P4/C6 的智能骑行码表。P4 负责 LVGL 图形、离线地图和骑行业务,C6 通过 ESP-Hosted SDIO 提供蓝牙 Controller。手机小程序通过 BLE 下发路线和定位,设备用分片状态机接收并校验路线,再把 GCJ-02 坐标转换到 WGS84 地图坐标,计算路线进度、偏离距离、转向和坡度。运动数据统一进入 app_motion_model,Cycling、MTB、地图、历史、本地播报和小智 MCP 都读取同一份快照。小智走 Wi-Fi/WebSocket/Opus,本地导航播报走 SD 卡预生成 PCM;所有网络、SD 和音频操作都放在独立任务,LVGL 线程只负责显示。
1.2 三分钟版本
项目的难点是多模块并发,而不是单独调用某个 SDK。输入有手机 BLE、GNSS、IMU、实体键和 Wi-Fi;处理有路线校验、坐标转换、地图索引、运动统计和语音;输出有圆形 AMOLED、BLE 状态、MAX98357A 音频和小智 MCP。我的做法是让每个服务拥有自己的状态和执行上下文,用有界队列传递命令/数据,用事件组表达阶段条件,用互斥锁保护短状态,用不可变快照给多个消费者读取。
路线举例:app_ble_gatt 收到写入后交给 app_ble_proto 解析,再由 app_ble_route 处理 0x22 BEGIN、0x23 DATA、0x24 COMMIT、0x25 ABORT。DATA 按 offset 接收,重复相同 offset 且内容一致时幂等,跳 offset、越界或内容冲突报错。COMMIT 检查协议版本、总长度、坐标范围、高程和 CRC32,成功后由路线任务做 GCJ-02 到 WGS84 反变换、地图投影、累计距离和包围盒计算。map_store 读取 SD 卡 map.idx/map.dat 的 3×3 网格,map_route 用当前位置到路线线段的最近投影算出进度和偏离,SD Route 页面只读快照绘制。
运动数据不会直接从 BLE 回调写 LVGL。手机定位命令先入有界队列,定位任务再更新 app_motion_model;页面、历史、本地播报和 MCP 都读模型快照。小智由双击电源键手动触发,Wi-Fi 获得 IP 后建立 WebSocket 和 Opus 音频;本地导航事件每秒检测一次,从 SD 读取 2048 字节 PCM 片段,P0 导航播报可打断 P1 指标播报和小智 TTS。LVGL 线程禁止 SD、网络、文件、编解码和阻塞锁等待,非 UI 线程禁止 lv_*。
2. 系统架构:面试官想听的不是模块列表
2.1 分层和依赖方向
我会把代码分成四层:
- 平台/板级:FreeRTOS、SD、NVS、ESP-Hosted、ICS43434、MAX98357A、显示和触摸。
- 领域服务:BLE GATT/协议/路线、地图文件、Wi-Fi、音频、历史。
- 业务模型:
app_motion_model、骑行状态、路线状态、活动类型、Xiaozhi 生命周期。 - UI:
ui_page_manager和各页面,只做布局、事件和快照到控件的映射。
UI 可以调用业务公开 API,但不能包含 SiFli/ESP BLE、Flash、socket 或文件写入头文件;业务/服务不能包含 lvgl.h 或持有 lv_obj_t *。这样做的价值是:地图或 BLE 的压力不会直接进入 UI 热路径,运动状态机可以脱离 LVGL 做主机测试,换芯片或换 RTOS 时只替换平台适配。
2.2 状态唯一所有者
BLE 管理模块拥有电源、广播、连接和路线传输状态;Wi-Fi 任务拥有扫描、连接、GOT_IP 和重试状态;Xiaozhi controller 拥有 chat/audio/mic 生命周期;map_store 拥有地图文件和网格快照;map_route 拥有路线投影数组和导航状态;app_motion_model 拥有运动指标;页面管理器拥有页面实例和导航栈。页面只读这些服务发布的状态,不能再保存第二套“已连接”布尔值。
2.3 一条数据的所有权链
SDK 回调参数通常只在回调期间有效,所以回调只做长度检查、最小复制和投递。队列拥有复制后的事件或固定缓冲槽;消费任务在处理完后归还槽位。服务发布快照时切换双缓冲或在短锁内完成索引交换;UI 只读取当前快照,不保存服务内部可变指针。页面销毁后,异步事件通过页面实例号/订阅号判断是否仍有效。
3. 地图和导航:怎么从原始数据走到圆屏
3.1 为什么需要 PC 端预处理
GeoJSON/OSM 原始数据是文本和层级对象,含字符串属性、浮点经纬度、线和面几何。让 P4 在运行时解析会同时占用 CPU、RAM 和 SD 随机 IO。tools/geojson_to_c_array_gui.py 在 PC 端流式扫描,使用 Shapely 做几何修复、简化、折线/面处理和多边形三角剖分,再用 SQLite 做中间归档,最后生成适合随机读取的 map.idx 和 map.dat。
3.2 坐标和文件格式
转换器使用局部等距圆柱近似,把经纬度变成以米为单位的局部世界坐标,地球半径为 6378137。默认网格边长 2000 m。map.idx 有 64 字节头和 8 字节网格索引项,用于定位 map.dat 块;map.dat 以 ODAT 头开头,后面是网格和几何数据。索引和数据分离后,设备先查偏移,再只读取当前位置需要的块。
3.3 运行时地图加载
map_store 是独立任务,启动时打开并检查两个文件的头、版本和长度。根据中心经纬度算网格编号,加载中心及周围八个网格。位置仍在当前覆盖范围时复用快照,跨网格时后台读取新块。页面不 fread、不查索引、不投影、不做三角剖分,只拿地图快照绘制。
3.4 GCJ-02 到 WGS84 为什么放在路线任务
小程序路线是 GCJ-02,离线地图是 WGS84。如果不转换,路线和道路会整体偏移。map_route 对每个点迭代求 WGS84,再调用 map_store_project_wgs84(),把结果放进世界坐标数组。转换只做一次,避免每帧重复三角函数计算。路线准备还生成累计距离、包围盒、高程数组、累计爬升和累计下降,后续 UI 和播报只读结果。
3.5 最近线段、偏离、转向和坡度
当前位置不是简单找最近路点,而是对路线相邻点形成的线段做点到线段投影,比较投影距离,得到最近线段及线段内比例。将该比例和累计距离相加,就有沿路线进度、剩余距离和偏离距离。相邻线段方向夹角超过 25 度时判定为转向。坡度使用约 25 m 的距离窗口计算,避免相邻高程噪声造成坡度跳变。
这是一套“沿既定路线跟随”的算法,不是在线路径规划器。偏航后会继续寻找原路线的最近位置并报告偏离,不会搜索新道路。面试官问“为什么不用 A*”时,回答是产品目标是路线跟随,路径规划在手机端完成;设备侧优先控制 RAM、实现复杂度和响应时间。
3.6 地图追问
问:为什么加载 3×3,不只加载一个网格?
答:位置在网格边缘时,道路和路线可能跨到邻格。预取 3×3 可以减少边界频繁重载,代价是 SD 读取和缓存占用增加;实际范围由网格大小和设备 RAM 权衡。
问:为什么要预计算累计距离?
答:运行时只需要最近线段索引和投影比例,就能 O(1) 得到已行驶路线距离,不必每次从起点累加所有线段。
问:地图卡顿怎么定位?
答:先区分文件读取、解析/投影、快照发布和 LVGL 绘制四段计时;检查是否跨网格、是否发生大块分配、是否更新了整个屏幕。不能只凭“LVGL 慢”下结论。
4. RTOS:每个任务为什么存在
4.1 任务分层
- LVGL adapter:指定核心运行,源码配置优先级 24、核心 1、最大延迟 5 ms,调用 LVGL 和页面定时器。
- 地图/路线:地图 IO、坐标转换和导航计算,分别拥有文件/路线状态。
- 定位/运动/历史:消费手机定位或 Demo 数据,更新运动模型和 NVS 历史。
- Wi-Fi/Xiaozhi:扫描连接、WebSocket chat 和 MCP 生命周期。
- 音频:麦克风采集与 Opus 编码、Opus 解码与播放、本地 PCM 播放。
- 低频任务:MCP 报告和本地播报事件检测约每 1000 ms 运行。
面试时要区分“实时性”和“高优先级”:UI 需要及时响应但不应阻塞;音频有固定消费节拍;BLE/地图/Wi-Fi 是异步 IO;MCP 是低频报告。不能说“所有线程都是实时线程”。
4.2 队列、事件组、互斥锁和通知怎么选
队列用于带数据且允许排队的事件,例如 BLE 包、定位命令、Wi-Fi 命令、Opus 包和播报事件;队列容量固定,满时按业务拒绝、合并或报错。事件组用于条件,例如 Wi-Fi ready、chat connected、chat restart、listen start,不承载大数据。任务通知用于一对一唤醒,例如地图中心变化。互斥锁只保护 chat handle、NVS 状态、双缓冲索引等短临界区,不持锁做网络、文件或 UI。
4.3 快照为什么比多个全局变量好
速度、距离、位置、路线进度等字段必须在一个一致时刻被读取。app_motion_model 写入时形成快照,页面、历史、本地播报和 MCP 读取一次性副本;MCP 文本使用双缓冲,最多 384 字节,超过 3000 ms 没有新数据就报告过期。这样避免读取到“新速度+旧距离”,也不会把 SDK 私有句柄传播到 UI。
4.4 并发追问
问:队列满了怎么办?
答:先按数据类型分类。位置/进度可以合并为最新快照;路线 DATA 不能静默丢包,应拒绝、保持 offset 或进入错误;Opus 包要统计溢出并停止/恢复播放;Wi-Fi 命令返回忙。所有策略都要让调用方知道是否真正受理。
问:如何避免优先级反转?
答:锁只保护短状态,避免高优先级 UI/音频等待低优先级任务持锁做 IO;必要时使用带优先级继承的互斥量,并通过 runtime stats 和锁持有时间实测,而不是无限提高优先级。
问:PSRAM 能不能放所有东西?
答:路线、地图、屏幕缓存和部分栈适合放 PSRAM;DMA 或实时音频缓冲要看芯片和驱动的可访问能力,不能只看到“有 PSRAM”就全部迁移。
5. BLE 路线和运动同步
5.1 P4/C6 分工
P4 运行 NimBLE Host、GATT 和业务;C6 提供 Bluetooth Controller;ESP-Hosted 通过 VHCI over SDIO 传输控制和事件。P4 本地 Controller 关闭,避免双控制器竞争。这个架构让 P4 有更多资源处理 LVGL/地图,但增加了 SDIO 链路、C6 重启和跨芯片断线处理。
5.2 路线协议如何保证可靠
Ride Service 有 Live Metrics、Command、Config、Ride Record、Route Status 等特征。Preferred MTU 为 247,但实际 DATA 长度仍按协商 MTU 检查。BEGIN 携带版本、活动类型、点数、总长度;DATA 携带 offset/序号和片段;COMMIT 携带最终确认信息;ABORT 取消。
接收端只接受期望 offset。重复相同 offset 且字节一致时重复确认,保证手机重试幂等;跳 offset、越界、同 offset 内容冲突立即报错。最大点数 2048;V1 单点 lat_e7 + lon_e7 共 8 字节,V2 增加高程等字段为 12 字节,最大 V2 载荷约 24576 字节。COMMIT 校验协议、长度、坐标、高程和 CRC32,通过后才变为 LOADED。
5.3 断线和定位
BLE 断线清空手机定位队列,标记定位超时并中止未完成路线;已经 LOADED 的路线保留,避免无线抖动导致路线丢失。手机定位事件进入有界队列,由定位任务更新 app_motion_model;页面不直接在 GATT 回调里更新位置或地图。
5.4 BLE 追问
问:CRC32 能不能保证安全?
答:CRC32 只能发现传输损坏,不能证明发送者身份,也不能防篡改。路线同步用它做完整性检查;量产 OTA 或敏感配置还需要签名/认证。
问:为什么不把完整路线一次 malloc 出来?
答:路线有明确最大点数,可以做固定上限和预分配;无上限 malloc 会在重连/取消/重复传输下造成碎片和峰值不可控。大数组放 PSRAM,但仍要在 BEGIN 时检查容量。
问:手机发送成功是否等于设备落盘成功?
答:不等于。进度应按设备已接收、校验或落盘的字节计算;只有 COMMIT 校验通过并发布 LOADED 才算业务成功。
6. 音频与本地语音
6.1 两条链路
ICS43434 输入 16 kHz、单声道、16-bit little-endian PCM;MAX98357A 输出同格式 PCM。小智上行由麦克风任务读取 PCM,计算平均绝对值做端点检测,连续多帧超过阈值开始监听,尾部静音达到条件后停止;随后以低复杂度、DTX 的 Opus 上传。下行 Opus 在回调中只复制到有界队列,由解码任务转 PCM 后播放。
本地播报不依赖网络。事件检测任务每 1000 ms 读取运动/导航快照,生成到达、左右转、每公里、每五分钟和速度变化事件;播放任务最多组合 48 个数字、单位和固定句子片段,从 /sdcard/navigation_audio 每次读 2048 字节后写 MAX98357A。
6.2 音频焦点
导航关键提示是 P0,里程/时间/速度是 P1。P0 可丢弃等待中的 P1,并调用 esp_xiaozhi_chat_send_abort_speaking() 中止小智 TTS。电话/HFP、A2DP 和提示音的最终抢占顺序应由统一 Audio Manager 决定;本地播报不能无条件打断通话。播报完成后根据状态决定是否恢复,不能盲目续播过期语音。
6.3 音频追问
问:为什么网络回调不直接解码?
答:回调上下文要快速返回,直接 Opus 解码会阻塞网络栈并造成回调重入风险。复制到固定队列后,由音频任务按节拍消费,队列满/空都能统计和恢复。
问:本地播报为什么不用 TTS?
答:导航关键提示需要离线可用和稳定延迟,预生成 PCM 不依赖 Wi-Fi/云端;代价是素材占 SD 空间、句式有限,缺素材时只能降级。
7. Wi-Fi 与小智
7.1 连接流程
开机不自动联网;实体电源键双击后打开确认浮层。Wi-Fi 任务扫描、排序,最多向 UI 提供 10 个 AP,最多保存 5 个配置。连接超时 20 秒,最多 3 次,等待 2 秒、4 秒重试。只有拿到 GOT_IP 才通知 AI voice。
controller 收到 GOT_IP 后调用 esp_xiaozhi_chat_get_info,检查 WebSocket 配置,创建 MCP engine,注册运动数据工具,初始化并启动 chat,打开 Opus 通道,再启动麦克风。当前固定 WebSocket,不使用 MQTT。退出时依次停用 AI voice、停止 chat/audio/mic、停止 Wi-Fi,再恢复进入前页面。
7.2 MCP 为什么只传摘要
MCP 每 1000 ms 读取一次运动快照,生成不超过 384 字节的中文报告,包含当前/平均/最高速度、距离、运动时间、累计爬升下降、路线进度和最近历史。双缓冲发布文本;快照超过 3000 ms 未更新时返回过期提示。这样既控制网络包和内存,也避免云端读取 SDK 指针、完整历史文件或高频传感器流。
7.3 小智追问
问:为什么不让页面直接连接 Wi-Fi?
答:连接、重试、NVS 和超时是服务职责,页面只发命令和显示状态。这样退出、断线和晚到事件有统一所有者,UI 不会因网络等待卡住。
问:关联上 AP 但没有 IP 能不能启动小智?
答:不能。只有 GOT_IP 才说明 DHCP/网络配置完成,否则 WebSocket 初始化会失败,状态机应继续等待或进入超时。
8. LVGL 页面与交互
8.1 页面职责
ui_page_manager 管理页面实例、进入/隐藏/销毁和返回;页面文件只写布局、样式、事件和 View Model 映射。Cycling/MTB 第一次 START 先判断是否有匹配活动类型的路线:有则进预览,无则直接开始记录;第二次 START 在 DEMO 启动模拟运动,在 PHONE/GNSS 启动真实记录。
SD Route 页面按 466×466 圆屏设计,预览范围约 600 m,导航范围约 430 m。导航状态约 20 ms 检查,业务文字约 500 ms 更新,地图绘制最小间隔约 33 ms;方向箭头预计算,屏幕和路线缓存放 PSRAM。缩放、平移、回到当前位置和滑动返回是页面视觉状态,不修改地图服务内部状态。
8.2 UI 线程纪律
所有 lv_obj 创建、修改、动画和删除只在 LVGL/UI 线程。BLE、Wi-Fi、音频、SD 和路线任务只能投递事件或更新快照。页面定时器只读快照并更新变化控件,不能 sleep、等锁、读文件、做坐标转换或图片解码。页面隐藏时停止无用动画和定时器;异步事件到达时先检查页面实例仍有效。
8.3 LVGL 追问
问:为什么不用每个 BLE 包刷新进度条?
答:BLE 包频率和 UI 帧频没有必要一致。业务层合并/限频,UI 只按 5-10 Hz 的可见频率更新;路线数据仍可高速接收,避免绘制成为吞吐瓶颈。
问:页面快速退出后回调怎么办?
答:回调不直接持有 lv_obj_t *,事件携带服务数据和页面实例/订阅代号。UI 线程消费时若实例已失效就丢弃 UI 更新,业务状态仍可保留。
9. 你可以主动讲的工程取舍
- 预处理换运行时资源:地图在 PC 端投影、简化和三角剖分,设备只做索引和局部读取。
- 快照换一致性:多个消费者读统一运动模型,不让每个页面各自解释定位数据。
- 有界队列换可预测性:宁可显式返回忙/溢出,也不让断网或 SD 变慢时内存无限增长。
- 本地 PCM 换离线可靠性:导航播报不依赖云端,但素材管理和 SD 空间变复杂。
- P4/C6 异构换资源隔离:P4 专注图形/地图,C6 专注 Controller,但需要处理 ESP-Hosted 跨芯片故障。
- 分层换可测试性:业务层不包含 LVGL,页面不能调用 SDK,后续可以用 mock 服务和主机测试状态机。
10. 诚实回答性能问题
可以确认的结构参数包括:LVGL adapter 优先级 24、核心 1、最大延迟 5 ms;路线任务栈宏 4096、优先级 4;BLE Preferred MTU 247;最大路线点数 2048;地图默认网格 2000 m、3×3 加载;本地播报检测 1000 ms、PCM 读块 2048 字节;MCP 报告 1000 ms、384 字节上限、3000 ms 过期。
不能直接声称的内容包括稳定 60 FPS、最差帧耗时、BLE 实际吞吐、Opus 端到端延迟、音频欠载率、PSRAM 峰值、续航和功耗。正确回答是:“代码上通过任务隔离、固定队列和刷新限频控制风险,具体数值需要在目标板用 LVGL monitor、FreeRTOS runtime stats、逻辑分析仪和功耗仪实测。”
11. 开发前扫描记录
- 扫描日期:2026-09-11。
- 扫描目录:
main/app_model、main/app_net、main/ai_voice、main/ui_app、bt、map、components/board_peripherals/audio、tools、doc。 - 关键文件:
main/main.c、app_motion_model.c、app_wifi.c、app_xiaozhi_controller.c、app_ai_voice*.c、app_ble*.c、map_store.c、map_route.c、ui_page_manager.c和各页面文件。 - 参考文档:地图二进制格式、SD 路线、BLE 总览/路线/定位、小智移植、手动联网、主动播报和手机定位说明。
- 未扫描/未验证:目标板帧率、功耗、无线吞吐、音频质量、长稳、手机互操作和云端服务内部。
12. 本次整理与验证边界
本手册只新增面试材料,不修改源码、协议、配置、资源、分区和构建产物。已静态核对上述路径、任务、状态、协议字段和参数;没有运行新的编译、单元测试、模拟器或目标板测试。面试中应把“静态确认”和“实机测量”明确分开。
13. 面试结束时的收束句
“我在这个项目里负责的不是把几个 Demo 拼起来,而是把路线协议、坐标转换、离线地图、运动状态、双芯片蓝牙、联网语音、本地播报和 LVGL 页面组织成一套有明确所有权的异步系统。最重要的设计原则是:回调快速返回、业务状态集中、UI 只消费快照、所有队列有上限、所有异步操作可取消和可恢复。”



