做展锐平台SensorHub的人,十有八九被这几个问题折磨过:传感器数据时有时无、功耗曲线怎么都压不下来、改一个加速度计驱动还得把整包固件重烧一遍。我前段时间在紫光展锐某平台上做SensorHub动态驱动加载与调试,这几个坑一个没落下。所谓SensorHub,就是SoC里面那颗低功耗协处理MCU,专门接管加速度计、陀螺仪、气压计和光学传感器,做完滤波和姿态计算之后,再把干净的数据交给应用处理器。而动态驱动加载,就是为了做到不重刷SensorHub整个固件,就能在运行状态里替换或者追加某个传感器的驱动模块。想法很简单,真正在展锐平台上落地时,牵扯到的驱动框架、内存布局、固件升级流程和调试手段,各个环节都比想象中要多。
这篇东西我尽量按实战线索写:从SensorHub的架构定位讲起,到动态加载的机制设计,再落到具体实现和调试方法,最后附上高频问题和排查套路。适合正在做嵌入式驱动、SensorHub移植和低功耗调优的开发者,尤其是手里正好有展锐平台资料但又被各种“玄学问题”卡住的朋友。
1. 先搞懂SensorHub在展锐平台是个什么角色
1.1 硬件侧:一颗专门干杂活的MCU
展锐平台在应用处理器之外,通常会集成一个低功耗MCU作为SensorHub,核心一般是Cortex-M系列,跑的不是Linux,而是一个很小的实时系统或者裸机调度器。它挂在传感器的中断线上,所有传感器的寄存器读写、数据搬运、中断处理都在这一侧完成。AP(应用处理器)想拿数据时不需要亲自去操作I2C或者SPI总线,而是通过mailbox、共享内存或者IPC和SensorHub通信。
这样做的好处非常直接:AP可以长时间处在低功耗状态,传感器这边每隔几毫秒醒来一次,把数据整理好放进环形缓冲区,等数据量够一次批处理或者AP主动来取的时候才发送出去。整机功耗的大部分来自于射频和AP侧的唤醒,SensorHub把传感器这部分高频中断全部消化掉,设备在息屏计步、抬手亮屏这类场景下,功耗差距就是从这里拉开的。
你没理解错,SensorHub就是让AP多睡觉的“值班员”。展锐平台把这个值班员放在SoC内部,和基带、GPU这些大件共享同一个封装,所以它和AP之间的通信延迟比外挂MCU低得多,也不用额外占用一颗独立芯片的物料成本。
1.2 为什么要把驱动拆出来动态加载,而不是一次编进去
早期SensorHub方案里,所有传感器驱动都是直接编译进固件里,固件里写死一组传感器型号和配置。这样做在大规模量产前问题不大,但一旦进入试产和售后阶段,就会出现各种麻烦:
- 同一块主板可能对应多套传感器组合,出货的时候需要按订单烧不同的固件版本,管理成本高。
- 传感器供应商替换型号时,硬件接口可能兼容,但寄存器配置和初始化时序不同,原来那版固件没法直接用。
- 设备已经出厂后在售后环节需要修复某个传感器驱动bug,按老方案只能整包固件升级,OTA包体积大、风险也大,万一升级到一半断电,SensorHub变砖的代价太高。
- 项目调试阶段,改一次驱动就要重新编译烧录整个SensorHub固件,一次烧录流程少则几分钟,多则十几分钟,反复迭代的时候效率极低。
动态驱动加载,就是把这些驱动从固件主体里抽离出来,单独编译成可加载模块。模块可以存放在AP侧的文件系统里,系统启动后按需下发到SensorHub,SensorHub跑一个“加载器”把模块放到指定RAM地址,完成符号绑定和初始化,驱动就能被调度器纳管。这样换传感器驱动不用动固件主体,调试效率高得多,OTA升级时也只需要下载一个小模块。
反过来讲,动态加载也不是没有代价。SensorHub的RAM通常只有几十到几百KB,加载器本身要占用空间,还要给驱动留出可重定位的代码段和数据段,内存布局比以前要精细得多。驱动模块由外部下发,还要考虑安全性问题,防止被篡改或者注入非预期的代码。做这个方案,本质上是拿一部分固件复杂度换灵活性和迭代效率。
2. 动态驱动加载的方案设计:先把地基打对
2.1 驱动接口抽象:用结构体把入口函数钉死
动态加载能不能做好,第一个关键点是驱动接口抽象得够不够稳。SensorHub侧的驱动,本质上是一组回调函数:初始化、轮询读取、睡眠、唤醒、事件中断处理。加载进来的驱动模块必须提供统一格式的描述符,加载器才能像插U盘一样把它识别出来。
在我们项目里,展锐平台沿用的思路是定义一个驱动描述符结构体,头部放魔数、驱动ID、版本号、名字,后面挂函数指针。下面这段是我们在调试时用的原型结构体,细节部分根据平台手册微调过:
typedef struct shub_drv_desc { uint16_t magic; /* 魔数,用于校验模块合法性,例如 0x4855 */ uint16_t drv_id; /* 传感器类型ID,对应加速度计/陀螺仪/气压计等 */ char name[32]; /* 驱动名字,辅助日志排查 */ uint32_t version; /* 驱动版本,做兼容性判断 */ int (*init)(struct shub_device *dev); int (*poll)(struct shub_device *dev, uint8_t *buf, uint16_t len); int (*suspend)(struct shub_device *dev); int (*resume)(struct shub_device *dev); int (*selftest)(struct shub_device *dev); void *priv; /* 驱动私有数据 */ } shub_drv_desc_t;这里有个容易被忽视的细节:所有函数指针的入参出参约定必须固定,加载器只认这份约定。有人图省事,在驱动里直接引用SensorHub固件里的全局变量,加载之后一调用就崩,原因就是动态加载的模块和静态链接不一样,符号解析规则更严格。驱动模块里除了调用加载器提供的基础函数(像log输出、延时、读写寄存器),不要自己声明外部全局变量去访问固件内部地址。
2.2 加载全流程:从主控侧文件到SensorHub调度器
动态加载不是一个函数就能搞定的动作,而是一条完整的链路。我们把流程拆成了四个阶段:
第一阶段是驱动模块的准备。传感器驱动源码用独立工程编译,输出目标文件时不直接链成最终固件,而是做成可重定位的ELF格式。编译选项上有讲究:要加上-fPIC或者至少保证代码段里不出现绝对地址跳转,数据段要按4字节甚至8字节对齐。这一步像是做个小型的动态库,但是目标运行环境是M核,没有MMU,所以重定位比Linux下的so要简单,但也更敏感。
第二阶段是模块的存储和校验。编译出来的驱动模块放在AP侧文件系统,比如/vendor/firmware/sensorhub/drvs/目录下。系统启动后,SensorHub管理进程会检查模块的版本和签名,确认合法后交给通信模块。签名这块我们吃过亏,最开始没做完整性校验,有次调试时模块在传输中途被截断,SensorHub加载到一半直接卡死,最后还是在模块头加了CRC32和简单的版本匹配才解决。
第三阶段是传输。AP侧通过mailbox把模块数据分块发送给SensorHub,SensorHub收到后写入预留的加载区RAM。这里要考虑带宽问题,一个驱动模块少则几KB,多则几十KB,mailbox单次传输的数据量通常有限,所以需要做分片和确认机制。实测下来,在展锐平台默认的mailbox配置下,一个16KB的模块大概需要几十毫秒传完,对于开机阶段来说完全可接受,但如果模块做得太大,这个时间会明显拖慢启动,所以要控制模块体积,压缩是有效的:我们最后在模块层做了轻量压缩,体积能减少40%左右。
第四阶段是SensorHub侧的加载和注册。加载器校验魔数和CRC,把代码段复制到RAM中可执行区域,数据段放到另一块区域,然后做重定位:把模块里的绝对地址引用修正为实际的RAM地址。全部就绪后,读模块描述符里的函数指针,调用init回调,注册到驱动管理器里。这样SensorHub里的传感器调度器就能通过drv_id找到这个驱动,开始正常轮询。
2.3 内存布局和符号解析:动态加载最容易翻车的地方
SensorHub的RAM非常紧张,动态加载要额外交出两块区域:一块给加载器自身运行,一块给驱动模块做code/data段。我们调试时遇到最多的崩溃,都和加载区域重叠有关。
展锐平台通常会给SensorHub的固件划分一个静态地址映射,比如Flash里的固件运行时拷贝到SRAM,从0x20000000开始放data,从某个固定地址放堆栈。动态加载区需要避开这些区域。我们的做法是在链接脚本里显式声明一个加载区段,比如DRV_LOAD_REGION,起始地址和大小在启动时打印出来,方便验证。驱动代码段复制进去之后,必须保证它的跳转指令用的是相对寻址,否则重定位逻辑要处理每一个绝对地址,复杂度暴增。
符号解析在这里很简单也很粗暴:驱动模块调用加载器提供的API函数,不是通过名字查找,而是通过一个导出表索引。固件启动时维护一张API函数指针表,驱动模块里对API的调用,编译时就转换成call [index]的形式,加载器在加载时把对应表项的地址填到模块的数据段里。这样省去了字符串符号查找的开销。说白了就是做一个简化版的PLT机制,虽然没有ELF的动态链接那么优雅,但在M核上足够可靠。
这里必须提醒一句:动态加载区的地址,一定要在驱动开发文档里明确写清楚,并且在驱动编译时用宏定义固化。我们项目里曾经因为不同分支的固件改了链接地址,驱动模块没跟着改,加载后一调API就跳到非法地址,查了一整天才定位到。
3. 实操实现:从驱动代码到SensorHub跑起来
3.1 驱动程序代码骨架
我以加速度计驱动的demo为例,展示动态加载模块里应该包含什么。驱动的工作逻辑很琐碎,但结构上就三块:注册时初始化硬件,轮询时读取数据,睡眠/唤醒时切换模式。
#include "shub_api.h" #define ACCEL_DEV_ADDR 0x18 #define ACCEL_REG_CTRL 0x2D #define ACCEL_REG_XOUT 0x32 static int accel_init(struct shub_device *dev) { /* 复位传感器,配置量程和采样率 */ shub_i2c_write(dev->bus, ACCEL_DEV_ADDR, ACCEL_REG_CTRL, 0x01); return 0; } static int accel_poll(struct shub_device *dev, uint8_t *buf, uint16_t len) { if (len < 6) return -1; return shub_i2c_read(dev->bus, ACCEL_DEV_ADDR, ACCEL_REG_XOUT, buf, 6); } static int accel_suspend(struct shub_device *dev) { shub_i2c_write(dev->bus, ACCEL_DEV_ADDR, ACCEL_REG_CTRL, 0x00); return 0; } static int accel_resume(struct shub_device *dev) { shub_i2c_write(dev->bus, ACCEL_DEV_ADDR, ACCEL_REG_CTRL, 0x01); return 0; } /* 模块导出描述符,加载器通过这个结构体找到驱动入口 */ const shub_drv_desc_t __attribute__((section(".drv_desc"))) accel_desc = { .magic = 0x4855, .drv_id = SHUB_DRV_ACCEL, .name = "accel_demo", .version = 0x0100, .init = accel_init, .poll = accel_poll, .suspend = accel_suspend, .resume = accel_resume, };注意这里用到了__attribute__((section(".drv_desc"))),这是为了让模块描述符固定在模块镜像的头部,加载器不用扫描整个模块找描述符,拿到模块先读头部偏移即可。这算是个约定俗成的优化,也方便我们调试时用十六进制工具直接查看模块内容,确认描述符有没有编译进去。
3.2 加载器的核心逻辑
加载器这边,处理的核心逻辑可以简化成下面几个步骤。我先给了伪代码,因为具体API跟展锐平台版本走,你们拿到对应SDK之后按这个思路填就行。
int shub_load_driver(const uint8_t *mod, uint32_t size) { shub_drv_desc_t *desc; uint32_t code_size, data_size; uint8_t *code_dst, *data_dst; /* 1. 校验魔数和CRC */ if (mod == NULL || size <= sizeof(shub_drv_desc_t)) return -EINVAL; if (memcmp(mod, "SHUB", 4) != 0) return -EBADF; /* 2. 解析头部,得到code和data段信息 */ parse_module_header(mod, &code_size, &data_size); /* 3. 在加载区分配内存 */ code_dst = drv_region_alloc(code_size, 4); data_dst = drv_region_alloc(data_size, 8); if (!code_dst || !data_dst) return -ENOMEM; /* 4. 复制段并做重定位 */ memcpy(code_dst, mod + code_off, code_size); memcpy(data_dst, mod + data_off, data_size); reloc_module(code_dst, data_dst); /* 5. 取描述符,调用init */ desc = (shub_drv_desc_t *)(code_dst); if (desc->init) desc->init(&new_dev); /* 6. 注册到驱动管理器 */ return drv_register(desc->drv_id, desc); }这里的drv_region_alloc是关键。加载区是一个环形缓冲池,每个驱动加载占用一块区域,卸载驱动时释放回缓冲池。多次加载卸载后会产生内存碎片,所以我们加了一个压缩整理机制:在注册管理器里维护一个空闲块列表,碎片过多时整块搬运已加载的驱动,同时更新驱动管理器里的指针。听起来麻烦,但在实际项目里这个动作不常发生,而且比直接重启SensorHub成本低得多。
3.3 编译脚本:模块怎么编出来
动态加载模块要能独立分发,编译脚本需要单独维护。我们用的编译方式是把驱动源码编成独立的elf文件,然后通过objcopy抽离出文本段和数据段。
关键编译参数有这几个:
-mcpu=cortex-m4:和SensorHub实际内核匹配,别用错,否则指令集兼容性会出问题。-fno-inline:保证函数指针地址稳定,避免优化器把函数内联后导致符号表异常。-fPIC:生成位置无关代码,这是动态加载的硬性要求。-nostdlib:不链接标准库,所有libc类功能由SensorHub导出表提供。-Wl,-T,drv.ld:指定链接脚本,把.drv_desc段放到镜像头部,把代码段和数据段分别对齐。
链接脚本是最后一道关口。我见过有驱动工程师直接把整个flash地址映射写死在链接脚本里,导致模块一加载就和加载区重叠,整个SensorHub直接hardfault。正确做法是链接脚本只定义相对地址,比如让代码段从0x00000000开始,数据段从0x00010000开始,加载器加载时再重定位到实际RAM地址。
4. 调试三板斧:日志、数据通路和底层工具
4.1 串口日志:第一现场
调试SensorHub驱动,我最先看的一定是日志。SensorHub侧打印日志的带宽很有限,但它是直接反映驱动运行状态的窗口。展锐平台的做法通常是SensorHub把日志写入一块共享内存环形缓冲,AP侧通过一个节点读取,比如/sys/kernel/debug/shub/log。调试时用ADB连上设备,不停抓日志就行。
adb shell "cat /sys/kernel/debug/shub/log"日志级别要会控制。SensorHub侧日志也可以分级,一般有ERROR、WARN、INFO、DEBUG四档。平时跑INFO就够,怀疑驱动初始化有问题时开DEBUG,打印每个寄存器读写和状态机切换。开DEBUG之后日志量会暴涨,如果不加缓冲经常会覆盖掉关键信息,所以我们调试时会额外加大共享内存环形缓冲,在调试版本里从4KB改成64KB,抓完日志再改回来。
这里要提醒一个反直觉的坑:SensorHub日志有时候不是即时刷到AP侧的,因为共享内存写入需要等AP侧主动来取,而AP侧后台读日志的进程如果睡眠了,日志就会积压在缓冲里。所以排查时如果发现日志“卡住”,先确认后台读取进程还活着,不要一上来就怀疑SensorHub死机。
4.2 数据通路验证:把传感器数据一条条追到底
驱动能加载、日志能打,不代表数据就是对的。我调试时永远先验证数据通路,再看算法和上报。数据通路的链路是:SensorHub驱动通过I2C/SPI读传感器寄存器 → 数据放到SensorHub内部缓冲区 → 按策略上报给AP → AP侧SensorService收到 → 应用拿到数据。
哪一环断了都会表现为“传感器没数据”。我习惯用一组连续操作来确认问题出在哪:
第一步,确认SensorHub驱动能不能裸读数据。在SensorHub日志里打桩,直接打印accel_poll读回来的6字节原始量。如果这6字节在传感器静止时波动几百个LSB,说明驱动配置或者硬件电路有问题;如果数据稳定,说明SensorHub这一侧是通的。
第二步,确认数据有没有真正上报到AP侧。在AP侧用getevent或者直接跑一个小的数据采集程序来读。如果你手头正好有串口调试助手这类工具,也可以临时在AP侧起一个socket服务把数据转发出来,用网络调试助手连上观测,虽然粗糙,但在没有标准sysfs节点的时候非常实用。
第三步,看数据有没有被SensorHub算法处理过。计步这类功能,SensorHub里跑的是算法,算法拿到的原始数据可能已经过时或者被中间处理过。这里要区分是驱动问题还是算法问题,方法很简单:在加载驱动后临时把算法模块的输入直接接上驱动输出,看算法节点有没有数据,没有就是算法侧的配置问题。
4.3 底层调试:TRACE32、JTAG和内核态的配合
动态加载驱动最头疼的问题,是SensorHub内核崩溃但没有现场日志。SensorHub是M核,崩溃时没有AP侧Linux的panic那么友好,很多时候只剩一个hardfault状态。这时候只能上调试器。
展锐平台的开发板上通常预留SWD或者JTAG接口,可以直接连TRACE32或J-Link。我们调试时用TRACE32连接SensorHub内核,核心步骤是:
- 加载SensorHub固件的ELF符号表,这样反汇编时可以看到函数名。
- 在动态加载区设置硬件断点,当PC指针进入加载区时触发,判断是哪个驱动的代码在执行。
- 崩溃后读取SCB寄存器组里的HFSR、CFSR、MMFAR等,定位是总线错误还是用法错误。
- 如果崩溃地址落在动态加载区,直接反汇编那段代码,结合加载时记录的重定位表,换算成驱动源码里的具体函数。
你可能会问:没有TRACE32怎么办?便宜一点的方案是J-Link加上Ozone,或者直接用pyOCD脚本配合gdb。SensorHub侧如果用gdb stub,常规gdb调试命令全套都能用,像bt看调用栈,info registers看寄存器值,x/20wx 0x20001000看内存数据。不过说实话,在SensorHub这种小型M核上,gdb stub本身占资源,我自己调试主力还是TRACE32,gdb用来做轻量确认。
调试时一个值得养成的习惯:每次加载驱动前,先打印加载区的起始地址和当前占用情况。一旦崩溃,对比崩溃地址是不是落在加载区里,可以快速判断问题到底在加载器还是驱动本身。这个做法救了我太多次。
4.4 把动态加载过程挂到AP侧日志里
不知道你们有没有这种经历:SensorHub侧日志和AP侧日志各看各的,两边时间还对不上,出了交叉问题根本理不清。后来我们统一了思路,在加载流程的关键节点,同时往AP侧日志里打一条带时间戳的记录,比如“send module start”“send module done”“load result 0”。
这样在排查时,把AP侧logcat和时间戳拉出来,就可以精确知道动态加载花了多长时间、在哪个阶段卡住。如果在AP侧看到整个加载过程都完成了,但SensorHub没起来,那不是传输问题,而是SensorHub内部加载器的问题,直接去SensorHub日志里看就行。两个日志交叉验证,定位速度快一倍。
这里用logcat命令的时候,要注意过滤tag,我们统一以SHUB开头打log,方便查看:
adb logcat -s SHUB_LOAD:I SHUB_DRV:I SHUB_ERR:E5. 高频问题速查表:按症状反查原因
以下是我在展锐平台上调试动态加载驱动时,见到最多的几类问题,以及对应的排查路径。
| 症状 | 可能原因 | 排查方法 | 解决办法 |
|---|---|---|---|
| 加载驱动后SensorHub崩溃 | 加载区地址重叠,或者重定位不完整 | TRACE32看PC崩溃地址,对比加载区范围 | 调整链接脚本;检查重定位表是否覆盖所有绝对地址 |
| 模块校验失败,加载被拒绝 | 魔数或CRC错误,传输过程中数据损坏 | 对比接收端和发送端的模块hexdump | 在驱动模块头增加CRC校验;检查mailbox分片传输的完整性 |
| 驱动init回调不执行 | 模块描述符里的函数指针为空,或者描述符段没有编进去 | 用hexdump工具查看模块头部是否包含描述符数据 | 编译时确认section(".drv_desc")被保留,检查链接脚本 |
| 传感器数据长时间不更新 | 轮询线程没有挂载到该驱动,或者驱动注册失败 | SensorHub日志看轮询列表,确认drv_id是否注册 | 检查驱动管理器注册代码,确认drv_id匹配 |
| I2C通信超时 | 传感器地址错误,GPIO复用配置问题,或者上拉电阻没焊接 | 示波器抓I2C波形;SensorHub日志打印ACK状态 | 确认硬件原理图;用IO配置工具重新配置引脚 |
| 睡眠后唤醒无数据 | suspend/resume回调没有正确设置传感器模式 | 在resume后手动触发一次poll,看是否有数据 | 检查resume回调的寄存器配置,确认传感器退出睡眠模式 |
| 动态加载后整个系统功耗偏高 | 驱动轮询频率太高,或者SensorHub频繁唤醒AP | 用功耗仪测整机电流,对比加载前后的变化 | 降低驱动poll频率,开启传感器FIFO或者中断唤醒模式 |
| 驱动升级后行为异常 | 新旧驱动内存布局不兼容,旧的数据段残留 | 卸载驱动后检查加载区是否清理干净 | 强化卸载流程,注册管理器里加上版本冲突检查 |
这个表格不是我为了凑数的,每一条都有真实事故在背后。特别是加载区重叠这个问题,我至少见过三次,每次都是改了SensorHub固件链接地址,忘了同步更新驱动工程的地址宏,然后出现“换固件后传感器全挂”的惨案。
6. 调试中的几个额外心得和工具习惯
6.1 串口调试助手这类工具在SensorHub调试里怎么用好
很多人觉得SensorHub是SoC内部的东西,串口调试助手派不上用场。实际上在开发板阶段,展锐平台一般会引出SensorHub的调试串口或者复用UART0,这种情况下串口调试助手就是最快看到SensorHub启动日志的方式。我习惯在开发阶段一直挂着串口,输出SensorHub的实时日志,不依赖AP侧共享内存的读取。
用串口工具时有几个细节要注意:
- 波特率要和SensorHub调试固件匹配,展锐平台常见的是115200或者921600,老平台还有38400的,先看代码里初始化串口的配置。
- 不要开工具的“自动发送”功能去刷SensorHub串口,SensorHub的串口接收缓冲区很小,刷多了容易把它的调试shell冲掉。
- 用带时间戳的串口工具,没有时间戳的日志分析交叉问题时很难对得上。很多免费的串口调试助手都带时间戳,打开就行。
6.2 ADB无线调试在SensorHub调试中的妙用
SensorHub调试一般要连着设备,但如果设备是带屏或者形态不适合经常插线,可以用ADB无线调试来减少物理连接。先把设备通过USB连一次,执行adb tcpip 5555,然后拔线,再用adb connect <device_ip>:5555连接。这样在桌面上就能完成日志拉取、驱动模块推送、服务重启一整套操作,连续调试一整天都不用碰一下设备。
当然,无线调试延迟比USB高,抓实时性要求高的日志时会有影响。我的做法是:常规操作走无线,抓精确时序时再插USB线。两者切换不受影响,因为ADB本身支持多连接。
adb connect 192.168.1.100:5555 adb push accel_demo.drv /data/local/tmp/ adb shell "shub_cli load /data/local/tmp/accel_demo.drv"6.3 把调试信息保存到日志文档,同时打印显示
SensorHub驱动调试有时候需要长时间跑一晚上,第二天再分析数据。如果只用终端窗口打印日志,要么终端断了日志就没了,要么窗口缓冲区覆盖丢数据。我的做法是在AP侧用脚本把日志同时存到文件和终端,用tee命令就能搞定:
adb logcat -s SHUB_LOAD:I SHUB_DRV:I | tee shub_debug_$(date +%m%d_%H%M).log这样一边实时盯着日志滚动,一边把完整内容落到文件里,跑完一夜也不怕丢。SensorHub侧串口日志也一样,用支持日志保存的串口工具,把整晚的日志存下来,分析那些“半夜两点传感器被异常唤醒”的问题时特别有用。
6.4 常规调试命令速查
最后整理一组日常高频命令,给刚开始接触展锐平台SensorHub的朋友当入口:
- 查看SensorHub固件版本:
adb shell "cat /sys/devices/platform/sensorhub/fw_version"。 - 查看已加载驱动列表:
adb shell "cat /sys/devices/platform/sensorhub/drivers"。 - 手动加载驱动模块:
adb shell "shub_cli load /data/local/tmp/xxx.drv"。 - 手动卸载驱动模块:
adb shell "shub_cli unload 0x0A"。 - 抓SensorHub共享内存日志:
adb shell "cat /sys/kernel/debug/shub/log"。 - 查看SensorHub占用内存:
adb shell "cat /sys/devices/platform/sensorhub/meminfo"。
7. 最后分享一点实战体会
SensorHub动态驱动加载这个东西,方案设计时有想象空间,真正落地时磨人的全是细节。我最大的感受是,动态加载并不是把编译方式从静态变动态就完事,它牵扯到内存布局、通信协议、固件管理、安全校验一整条链路,每一环都值得在实际项目中留下文档。尤其是内存布局,一定要把“加载区在哪里、谁负责分配、崩溃怎么查”写清楚,否则换人维护时,多数时间都会浪费在重复踩坑上。
调试工具这块,我的建议是串口日志打底、TRACE32攻坚、ADB做日常效率工具。不要指望一种工具解决所有问题,SensorHub这种小型MCU上的问题往往要多种工具交叉验证。如果你手里正好在做展锐平台的SensorHub相关开发,这篇内容里提到的流程和坑,希望能帮你少走几段弯路。