项目介绍
本次项目我们围绕 NXP 的 FRDM-MCXN236 平台,开发一款符合 mikroBUS 标准的扩展板. 扩展板上主要带有 SSD1306 的 OLED 屏幕和蜂鸣器,配合 FRDM 底板带有的加速度计,实现了一个带有显示和警报功能的水平仪,软件运行于 Zephyr 实时操作系统上. 同时扩展板还额外预留了 MCP4728、ADS1115、ESP8266 的焊盘和外围电路,可以再实现 A/D、D/A 和 Wi-Fi 功能.


mikroBUS 标准
mikroBUS 是由 MIKROE 公司发起的一个扩展板标准,定义了扩展板的电气连接接口参数、引出功能、尺寸建议等,截至本文写作最新修订为 2.0.0 [1]. 目前 NXP 的许多 FRDM 开发板都标配了符合 mikroBUS 母板标准的接口,是较多使用这个扩展板标准的厂商.
mikroBUS 官方设有三种推荐的扩展板规格,其中 S 和 M 规格是给大多数传感器、收发器设计考虑的规格,但如果有大量连接器的话,就可能需要考虑 L 规格的板子了.

准备工作
Zephyr 环境配置
我们使用 podman compose 来配置 Zephyr SDK 开发环境,镜像版本 v0.28-branch 的 Zephyr SDK 是 v0.17.4,要根据 SDK 版本选择操作系统的版本.
x-podman:
in_pod: false
services:
zephyr:
image: ghcr.io/zephyrproject-rtos/zephyr-build:v0.28-branch
hostname: zephyr
container_name: zephyr
tty: true
volumes:
- $HOME/projects/funpack54:/app:z
- /dev/ttyACM0:/dev/ttyACM0
- /dev/bus:/dev/bus
user: 1000:1000
userns_mode: keep-id:uid=1000,gid=1000
tmpfs:
- /tmp:size=1G
working_dir: /app
networks:
default:
enable_ipv6: true
这里我们选择 Zephyr RTOS v4.3.1,使用 git submodule 管理依赖.
git submodule add -b v4.3.1 https://github.com/zephyrproject-rtos/zephyr.git vendor/zephyr
LinkServer 配置
我们在使用 west flash 烧录程序时,发现需要 NXP 的私有 LinkServer 程序,可以前往 [2] 下载.

可按照下面的命令进行安装,这样可以避免 whiptail 的依赖.
# ./LinkServer_25.3.31.x86_64.deb.bin -- acceptLicense
软件设计和实现
系统架构
应用分为两条数据路径,共用同一份加速度计采样,水平仪显示交互方面的流程为

警报的流程则为

两条路径的处理节奏不同:显示路径追求平滑和趣味性,每帧都要更新;警报路径追求稳定和可靠,只在状态翻转时才驱动蜂鸣器.
加速度计数据流水线
加速度计输出三轴原始值,先做一阶低通滤波,再归一化:
g = sqrt(ax² + ay² + az²)
nx = ax / g
ny = ay / g
nz = az / g
设备水平时,重力全在 z 轴,nx ≈ ny ≈ 0. 倾斜时,重力在 x/y 轴出现分量,nx / ny 偏离零. 这两个分量就是后续所有计算的输入.
重力小团子物理模型
直接把倾斜角线性映射成屏幕偏移,手感很“塑料”:没有惯性,没有过冲,没有摩擦. 我们改用弹簧-阻尼质点模型:在屏幕后面放一个虚拟小球,让它在一个盆地里滚动.
目标位置
先用双曲正切函数把 nx / ny 映射成目标位置:
target_x = R * tanh(k * nx)
target_y = R * tanh(k * ny)
R 是归一化半径,k 是灵敏度. tanh 的特点是中心附近近似线性、大角度时饱和,保证中心灵敏的同时点不会飞出屏幕.
如果倾斜角在死区以内(约 1.5 deg),目标位置直接设为零,避免水平附近来回漂移.
弹簧力
弹簧力正比于目标位置和当前位置的差:
F = k_spring * (target - p)
p 是小球当前位置. 差越大,弹簧拉得越用力. 水平时 target = 0,弹簧自然把小球拉回中心.
摩擦
如果小球速度很小,且弹簧力还没超过静摩擦阈值,速度直接设为零,小球卡住不动. 如果已经在滑动,则施加动摩擦力,方向与速度相反,大小固定. 动摩擦略小于静摩擦,一旦开始滑动就更容易继续.
积分
用半隐式欧拉法更新速度和位置:
v = (v + F * dt) / (1 + damping * dt);
p = p + v * dt;
分母里的 1 + damping * dt 把阻尼直接融进速度更新,比显式写法更稳定,不容易发散.
中心吸附
如果目标位置在死区内,且小球已经很接近中心,额外施加一个收束因子,让位置和速度快速衰减到零. 这一步解决静摩擦可能把小球卡在中心附近某个小偏移上的问题.
警报逻辑
我们直接用质点投影后的坐标 (nx, ny) 的模长作为倾斜指标,当大于特定阈值时判断为警报
magn = sqrt(nx² + ny²)
if (magn > threshold) {
alarm();
}
屏幕映射
物理模型输出归一化坐标 (nx, ny),范围约 [-1, 1],中心为 0,统一映射到屏幕像素:
screen_cx = LCD_WIDTH / 2;
screen_cy = LCD_HEIGHT / 2;
screen_r = min(LCD_WIDTH, LCD_HEIGHT) / 2 - 20;
px = screen_cx + (int)(nx * screen_r);
py = screen_cy + (int)(ny * screen_r);
screen_r 是归一化 1.0 对应的像素数,取内切圆半径保证任何方向倾斜点都不会跑出屏幕,减 10 是留边距. LCD_WIDTH 和 LCD_HEIGHT 通过设备树别名直接获取:
#define OLED_NODE DT_ALIAS(oled)
#define LCD_WIDTH DT_PROP(OLED_NODE, width)
#define LCD_HEIGHT DT_PROP(OLED_NODE, height)
Zephyr 设备树
Zephyr 推荐使用设备树的方式来进行外设的描述、初始化和配置,这样分离了系统设备关系和代码,让复杂系统变的更好维护,我们用下面的 DTS 可以发现 FRDM 底板的加速度计,同时配置了 SPI 上的 128x64 OLED. 配置过程并不是一帆风顺的,可参考故障排查的踩坑经验.
/ {
aliases {
oled = &ssd1306;
accel = &fxls8974;
adc = &ads1115;
dac = &mcp4728;
buzzer = &bz;
spi = &flexcomm3_lpspi3;
};
buzzers {
compatible = "gpio-leds";
bz: bz {
gpios = <&gpio4 18 GPIO_ACTIVE_HIGH>;
label = "Buzzer";
};
};
chosen {
zephyr,display = &ssd1306;
};
};
&flexcomm2_lpi2c2 {
status = "okay";
pinctrl-0 = <&pinmux_flexcomm2_i2c>;
pinctrl-names = "default";
clock-frequency = <I2C_BITRATE_STANDARD>;
#address-cells = <1>;
#size-cells = <0>;
ads1115: ads1115@49 {
compatible = "ti,ads1115";
reg = <0x49>;
status = "okay";
#address-cells = <1>;
#io-channel-cells = <1>;
#size-cells = <0>;
};
mcp4728: mcp4728@60 {
compatible = "microchip,mcp4728";
reg = <0x60>;
status = "okay";
#io-channel-cells = <1>;
voltage-reference = <0 0 0 0>;
power-down-mode = <0 0 0 0>;
};
fxls8974: fxls8974@18 {
compatible = "nxp,fxls8974";
reg = <0x18>;
status = "okay";
// interrupt-parent = <&gpio0>;
// interrupts = <21 8>;
};
};
&pinmux_flexcomm3_lpspi {
group0 {
pinmux = <FC3_P0_PIO1_0>, // MOSI
<FC3_P1_PIO1_1>, // SCK
<FC3_P4_PIO1_16>; // PCS3
// MISO used for SSD1306 DC
// <FC3_P2_PIO1_2>
slew-rate = "fast";
drive-strength = "low";
};
};
&gpio5 {
status = "okay";
};
&flexcomm5_lpi2c5 {
status = "disabled";
/delete-property/ pinctrl-0;
/delete-property/ pinctrl-names;
};
&i3c1 {
status = "disabled";
/delete-property/ pinctrl-0;
/delete-property/ pinctrl-names;
};
&lpcmp0 {
status = "disabled";
/delete-property/ pinctrl-0;
/delete-property/ pinctrl-names;
};
&flexcomm3_lpspi3 {
status = "okay";
pinctrl-names = "default";
clock-frequency = <2000000>;
#address-cells = <1>;
#size-cells = <0>;
ssd1306: ssd1306@3 {
compatible = "solomon,ssd1306fb";
reg = <3>; // PCS3
status = "okay";
spi-max-frequency = <2000000>;
data-cmd-gpios = <&gpio1 2 GPIO_ACTIVE_HIGH>;
// addon board reset
reset-gpios = <&gpio5 2 GPIO_ACTIVE_LOW>;
width = <128>;
height = <64>;
segment-offset = <0>;
page-offset = <0>;
display-offset = <0>;
multiplex-ratio = <63>;
prechargep = <0xF1>;
segment-remap;
com-invdir;
};
};
功能验证
水平放置时,质点中心归位,屏幕左上角显示的时目前加速度的模长,用于诊断,实际的阈值判断逻辑参考的是弹簧-阻尼质点模型的质点位置.

当我们倾斜到一定程度时,质点偏离中心,并远离内圈的阈值等高线,此时 LED 灯熄灭,蜂鸣器开始鸣叫,这一部分视频中有完整演示,可前去查看.

故障排查
GPIO5 时钟缺少

mikroBUS 上的 RST 引脚实际由 P5_2 驱动,为了使用 GPIO5 需要
&gpio5 {
status = "okay";
}
编译时会报错找不到 kCLOCK_Gpio5,这一段报错来自 Zephyr 的 FRDM-MCXN236 开发板初始化代码.

zephyr/boards/nxp/frdm_mcxn236/board.c:185:27: error: 'kCLOCK_Gpio5' undeclared (first use in this function); did you mean 'kCLOCK_Gpio4'?
194 | CLOCK_EnableClock(kCLOCK_Gpio5);
我们在官方 issue tracker 上发现同系列的 FRDM-MCXN947 有类似的问题 [3],解决方法是移除相应的 CLOCK_EnableClock 的代码行,因为 GPIO5 的时钟域是和 VBAT 在一起的,不需要单独启用.
diff --git a/boards/nxp/frdm_mcxn236/board.c b/boards/nxp/frdm_mcxn236/board.c
index f4034aeb..976b38aa 100644
--- a/boards/nxp/frdm_mcxn236/board.c
+++ b/boards/nxp/frdm_mcxn236/board.c
@@ -182,7 +182,7 @@ void board_early_init_hook(void)
#endif
#if DT_NODE_HAS_STATUS_OKAY(DT_NODELABEL(gpio5))
- CLOCK_EnableClock(kCLOCK_Gpio5);
+ // CLOCK_EnableClock(kCLOCK_Gpio5);
#endi
外设抢占
接入 OLED 后,使用 Zephyr 的 DTS 编写 SSD1306 参数,发现始终点不亮,万用表排查到 OLED 的 CS引脚从未拉低选中. 发现一旦去除 CONFIG_I2C=y 关闭 I2C 子系统后,CS 就能正常拉高. 本质上是因 MCXN236 的 Zephyr 官方 DTS 中,有许多外设默认打开,并且 pin 是冲突的,比如最典型的一个 I2C 外设占用了 FC5_P0_PIO1_16 即 P1_16 的 pinmux 复用.
# boards/nxp/frdm_mcxn236/frdm_mcxn236-pinctrl.dtsi
pinmux_flexcomm5_lpi2c: pinmux_flexcomm5_lpi2c {
group0 {
pinmux = <FC5_P0_PIO1_16>,
<FC5_P1_PIO1_17>;
slew-rate = "fast";
drive-strength = "low";
input-enable;
bias-pull-up;
drive-open-drain;
};
};
# boards/nxp/frdm_mcxn236/frdm_mcxn236.dts
&flexcomm5_lpi2c5 {
pinctrl-0 = <&pinmux_flexcomm5_lpi2c>;
pinctrl-names = "default";
clock-frequency = <I2C_BITRATE_STANDARD>;
};
&flexcomm5_lpi2c5 {
status = "okay";
};
根据开发板的官方指南,可以看到 P1_16 同时物理上连接了 mikroBUS 接口的 CS 引脚和 Arduino 的 I2C 接口. 但是 Zephyr 的开发板 DTS 中只开启了相应接口的 I2C 外设,这就导致 pinmux 的 P1_16 在开机初始化的时候就被抢走.
同时用于 MOSI 的 P1_0 在使用逻辑分析仪分析后发现一开始数据正确,后面就数据错乱,也怀疑是中途被 lpcmp0 外设抢走,关闭后数据正常.



解决方法,将这些额外的外设进行关闭,其中 lpi2c5 和 i3c1 会抢占 P1_16,而 lpcmp0 会抢占 P1_0.
&flexcomm5_lpi2c5 {
status = "disabled";
/delete-property/ pinctrl-0;
/delete-property/ pinctrl-names;
};
&i3c1 {
status = "disabled";
/delete-property/ pinctrl-0;
/delete-property/ pinctrl-names;
};
&lpcmp0 {
status = "disabled";
/delete-property/ pinctrl-0;
/delete-property/ pinctrl-names;
};
CS 时序问题
基本的引脚问题解决了,但是发现 OLED 还是点不亮,再次用逻辑分析仪发现,SPI 在传输第一个字节的时候,`CS` 中途被莫名其妙拉高,后续 CS 就正常.

后续查阅资料发现在使用 GPIO 模式的 CS 控制,即 SPI 控制器的 cs-gpios 字段时,可能存在时序问题 [4],需要使用 SPI 外设自带的 CS 控制后解决这个问题.
// before
&pinmux_flexcomm3_lpspi {
group0 {
pinmux = <FC3_P0_PIO1_0>, // MOSI
<FC3_P1_PIO1_1>; // SCK
// MISO used for SSD1306 DC
// <FC3_P2_PIO1_2>
slew-rate = "fast";
drive-strength = "low";
};
};
&flexcomm3_lpspi3 {
status = "okay";
pinctrl-names = "default";
clock-frequency = <2000000>;
cs-gpios = <&gpio1 16 GPIO_ACTIVE_LOW>;
ssd1306: ssd1306@0 {
compatible = "solomon,ssd1306fb";
reg = <0>;
//...
}
}
// after
&pinmux_flexcomm3_lpspi {
group0 {
pinmux = <FC3_P0_PIO1_0>, // MOSI
<FC3_P1_PIO1_1>, // SCK
<FC3_P4_PIO1_16>; // PCS3
// MISO used for SSD1306 DC
// <FC3_P2_PIO1_2>
slew-rate = "fast";
drive-strength = "low";
};
};
&flexcomm3_lpspi3 {
status = "okay";
pinctrl-names = "default";
clock-frequency = <2000000>;
ssd1306: ssd1306@3 {
compatible = "solomon,ssd1306fb";
reg = <3>; // PCS3
//...
}
}

后记
其实本来想用 MCP4728 和 ADS1115 做一个 D/A + A/D 扩展板的,但是 MCP4728 老是烧(上电后一开始正常,之后就发热,I2C 总线就被拉低了),不知道是不是焊接的时候产生热应力了,也希望评论区大神指出问题.

总结展望
本次 Funpack 活动,让我熟悉了 mikroBUS 标准与扩展板的绘制,同时接触 Zephyr 实时操作系统让我进一步了解嵌入式领域最新的开源社区动向,之后编写 DTS 设备树更加得心应手.
之后考虑单独购买 MCP4728 诊断一下没有解决的烧芯片问题.
参考资料
[1]: https://download.mikroe.com/documents/standards/mikrobus/mikrobus-standard-specification-v200.pdf
[2]: https://www.nxp.com/design/design-center/software/development-software/mcuxpresso-software-and-tools-/linkserver-for-microcontrollers:LINKSERVER
[3]: https://github.com/zephyrproject-rtos/zephyr/pull/81730
[4]: https://github.com/zephyrproject-rtos/zephyr/issues/110302