在FRDM_MCXA366上有 OD-6010 段式 LCD 。显示屏为 OD-6010 6 位 7 段 + 6 个圆点 LCD 玻璃面板。
驱动在 Zephyr 4.4.0 上运行。
驱动直接调用 NXP MCUX SDK 的 fsl_slcd.h HAL( ),不修改 Zephyr 内核或设备树。
CMake 负责把 HAL 源文件 fsl_slcd.c 编入 Zephyr 镜像(见 )。
1. 硬件概览
| SoC | MCXA366(Cortex-M33) |
| LCD 控制器 | SLCD0(基地址 0x400C2000,48 个波形字节 WF8B[0..47]) |
| 玻璃 | OD-6010,6 位 7 段,TN 正显,1/4 duty、1/3 bias |
| 段数 | 6 数字 × 7 段 + 6 个圆点(DP1..DP6) |
| 显示区尺寸 | 27.60 × 11.60 mm;数字高度 8 mm |
| 背板 | COM0..COM3 共 4 路 |
引脚全部走 PORT0,mux 设置为 ALT9(LCD 功能),无需外接电阻或电荷泵器件,SLCD 内部 VLL3 供电。
2. SLCD 引脚映射
SLCD 外设一共 48 个引脚(P0..P47),本项目只用到 16..31,全部经过 PORT0 的 12..27 号 PAD。
SLCD pin 角色 GPIO pad
-------- --------------- --------
16..27 12 路段信号 P0_12..P0_23
28..31 4 路背板 (COM0-3) P0_24..P0_27
k_pin_map[]()把这 16 个 SLCD 引脚逐个路由到 PORT0:
static const slcd_pin_map_t k_pin_map[] = {
/* 12 segment pins */
{16U, 0U, 12U}, {17U, 0U, 13U},
{18U, 0U, 14U}, {19U, 0U, 15U},
{20U, 0U, 16U}, {21U, 0U, 17U},
{22U, 0U, 18U}, {23U, 0U, 19U},
{24U, 0U, 20U}, {25U, 0U, 21U},
{26U, 0U, 22U}, {27U, 0U, 23U},
/* 4 backplane pins */
{28U, 0U, 24U}, {29U, 0U, 25U},
{30U, 0U, 26U}, {31U, 0U, 27U},
};
slcd_init() 在配置 SLCD 之前先遍历这张表,调用 PORT_SetPinMux(PORT0, pad, kPORT_MuxAlt9) 把每个 PAD 切到 LCD 功能()。
3. 玻璃面板(OD-6010)的物理布局
3.1 数字顺序
OD-6010 把数字从右到左编号,物理上从左到右是:
dig6 dig5 dig4 dig3 dig2 dig1
↑ ↑ ↑ ↑ ↑ ↑
左 右
所以 NXP 的 NUM_POS1(逻辑 pin 1 = SLCD pin 16)在玻璃上对应最右的那一位。
驱动对外约定的 pos 索引却是左到右(slcd_show_string("123456") 让 "1" 显示在最左)。为消除歧义,PIN_ABCN/PIN_EFG 宏把逻辑引脚反向映射:
/* PIN_ABCN(x) / PIN_EFG(x) — 1-indexed digit → SLCD pin index.
* 因为玻璃从右到左编号 1..6,要把"我的位置 0 = 最左"
* 对齐到物理最左的 dig6 (SLCD pin 26/27),所以取反方向。 */
#define PIN_ABCN(x) (uint32_t)(28U - 2U * (x)) /* SLCD pin 26,24,22,20,18,16 */
#define PIN_EFG(x) (uint32_t)(29U - 2U * (x)) /* SLCD pin 27,25,23,21,19,17 */
| 0(左) | 1 | 26 | 27 | dig6 |
| 1 | 2 | 24 | 25 | dig5 |
| 2 | 3 | 22 | 23 | dig4 |
| 3 | 4 | 20 | 21 | dig3 |
| 4 | 5 | 18 | 19 | dig2 |
| 5(右) | 6 | 16 | 17 | dig1 |
3.2 1/4 duty 与背板相位
1/4 duty 表示在任意时刻 4 个 COM 里只有 1 个激活。SLCD 控制器用 4 个相位 A/B/C/D 循环激活 4 路背板:
static const slcd_phase_type_t k_digit_phase[4] = {
PHASE_A, PHASE_B, PHASE_C, PHASE_D,
};
static const uint32_t k_com_pin[4] = { 28U, 29U, 30U, 31U };
static void program_backplanes(void)
{
for (uint32_t d = 0; d < 4U; d++) {
SLCD_SetBackPlanePhase(LCD0, k_com_pin[d], k_digit_phase[d]);
}
}
SLCD_SetBackPlanePhase(LCD0, 28, kSLCD_PhaseAActivate) 的意思是:SLCD 引脚 28(COM0)在相位 A 时为有效电平。具体哪一段在哪个相位亮,由 WF8B[] 字节里设置的位决定。
3.3 段信号与相位的对应
OD-6010 的段信号在 SLCD 控制器侧的相位编码(参考 NXP slcd/OD-6010.c):
| 0 | A_COM | a | P_COM | DP(圆点) |
| 1 | B_COM | b | F_COM | f |
| 2 | C_COM | c | G_COM | g |
| 3 | D_COM | d | E_COM | e |
EFG 引脚的相位集合与 ABCN 不对称:bit 0 是 DP(不是 f),所以 SEGF_EFG 必须定义为 (1U << 1) 而非常见的 (1U << 0):
#define SEGF_EFG (1U << 1) /* upper-left vertical */
#define SEGG_EFG (1U << 2) /* middle horizontal */
#define SEGE_EFG (1U << 3) /* lower-left vertical */
4. 字模表
每个数字用两字节描述:一个写到 PIN_ABCN,一个写到 PIN_EFG:
/* k_digit_abcn: a/b/c/d 段 */
static const uint8_t k_digit_abcn[16] = {
[0] = SEGA_ABCN | SEGB_ABCN | SEGC_ABCN | SEGD_ABCN, /* 0 = ABCD */
[1] = SEGB_ABCN | SEGC_ABCN, /* 1 = BC */
[2] = SEGA_ABCN | SEGB_ABCN | SEGD_ABCN, /* 2 = ABD */
[3] = SEGA_ABCN | SEGB_ABCN | SEGC_ABCN | SEGD_ABCN, /* 3 = ABCD */
[4] = SEGB_ABCN | SEGC_ABCN, /* 4 = BC */
[5] = SEGA_ABCN | SEGC_ABCN | SEGD_ABCN, /* 5 = ACD */
[6] = SEGA_ABCN | SEGC_ABCN | SEGD_ABCN, /* 6 = ACD */
[7] = SEGA_ABCN | SEGB_ABCN | SEGC_ABCN, /* 7 = ABC */
[8] = SEGA_ABCN | SEGB_ABCN | SEGC_ABCN | SEGD_ABCN, /* 8 = ABCD */
[9] = SEGA_ABCN | SEGB_ABCN | SEGC_ABCN | SEGD_ABCN, /* 9 = ABCD */
};
/* k_digit_efg: e/f/g 段 */
static const uint8_t k_digit_efg[16] = {
[0] = SEGE_EFG | SEGF_EFG, /* 0 = EF */
[1] = 0U, /* 1 = - */
[2] = SEGE_EFG | SEGG_EFG, /* 2 = EG */
[3] = SEGG_EFG, /* 3 = G */
[4] = SEGF_EFG | SEGG_EFG, /* 4 = FG */
[5] = SEGF_EFG | SEGG_EFG, /* 5 = FG */
[6] = SEGE_EFG | SEGF_EFG | SEGG_EFG, /* 6 = EFG */
[7] = 0U, /* 7 = - */
[8] = SEGE_EFG | SEGF_EFG | SEGG_EFG, /* 8 = EFG */
[9] = SEGF_EFG | SEGG_EFG, /* 9 = FG */
};
数字 3/4/7/9 在 EFG 表里只用部分段;6/8 用全部三段,这与标准 7 段字模完全一致。
刷新一个位置的实现:
static void refresh_digit(uint32_t pos)
{
uint8_t glyph = s_digit_glyph[pos];
if (glyph > 9U) {
glyph = 0U; /* 超范围当作空白 */
s_digit_glyph[pos] = 0U;
}
SLCD_SetFrontPlaneSegments(LCD0, PIN_ABCN(pos + 1U), k_digit_abcn[glyph]);
SLCD_SetFrontPlaneSegments(LCD0, PIN_EFG(pos + 1U), k_digit_efg[glyph]);
}
SLCD_SetFrontPlaneSegments(base, pin, value) 把 value 直接写到 WF8B[pin],一次写入 8 位,刚好对应一个 SLCD 引脚上的 8 个相位(A..H)。
5. 小数点(DP)
OD-6010 的 6 个圆点分布在 3 个空隙内,每个空隙一上一下:
位置 上方点 下方点 物理位置(在 dig5-dig4 间隙)
↑ ↓ ↓
dig6 dig5 ·P1· dig4 ·P2· dig3 ·P3· dig2 dig1
·P4· ·P5· ·P6·
每个圆点在 SLCD 物理上借用某一位的 EFG 引脚:在 WF8B[pin] 字节里设置 bit 0(= P_COM 相位),玻璃就把那个交叉点的圆点点亮。圆点位置与 EFG 引脚的对应:
| DP1 | 23 | 上方,位置 1-2 之间 |
| DP2 | 21 | 上方,位置 2-3 之间 |
| DP3 | 19 | 上方,位置 3-4 之间 |
| DP4 | 25 | 下方,位置 1-2 之间 |
| DP5 | 27 | 下方,位置 2-3 之间 |
| DP6 | 17 | 下方,位置 3-4 之间 |
k_dp_pin[]()就是这张表的程序化版本。
刷新逻辑——DP 和数字共享一个 SLCD 引脚,所以必须把 DP 的相位 A 位与当前字模合并后再写:
static void refresh_dp(uint32_t icon_id)
{
uint32_t slcd_pin = k_dp_pin[icon_id];
uint32_t efg_digit = (27U - slcd_pin) / 2U; /* 反向 PIN_EFG */
uint8_t glyph = s_digit_glyph[efg_digit];
uint8_t efg_phase = k_digit_efg[glyph]; /* 段 f/g/e */
if (s_dp_glyph[icon_id] != 0U) {
efg_phase |= (uint8_t)PHASE_A; /* bit 0 = DP 相位 */
}
SLCD_SetFrontPlaneSegments(LCD0, slcd_pin, efg_phase);
}
每当 slcd_show_number() 改了一个数字,会调用 refresh_dp_for_digit(pos) 把跟该数字共享 EFG 引脚的所有 DP 重新合并一遍,避免覆盖掉已经点亮的圆点。
6. SLCD 控制器初始化
slcd_init() 流程如下():
GPIO 路由:循环 k_pin_map[],把每个 PAD 切到 kPORT_MuxAlt9。
解除外设复位:RESET_ReleasePeripheralReset(kSLCD0_RST_SHIFT_RSTn)——上电后 SLCD 默认处于复位状态,必须先释放才能访问寄存器。
开时钟:CLOCK_EnableClock(kCLOCK_GateSLCD0)。HAL 在 FSL_SDK_DISABLE_DRIVER_CLOCK_CONTROL=1 模式下不会自己开时钟,所以这里手动开。
填配置:
cfg.dutyCycle = kSLCD_1Div4DutyCycle;
cfg.slcdLowPinEnabled = 0xFFFF0000U; /* 只使能 P16..P31 */
cfg.backPlaneLowPin = 0xF0000000U; /* P28..P31 设为背板 */
cfg.clkPrescaler = kSLCD_ClkPrescaler04; /* 32 kHz / 64 ≈ 64 Hz 帧率 */
cfg.voltageTrimVLL1 = kSLCD_VolatgeTrimNo; /* 不调对比度 */
cfg.voltageTrimVLL2 = kSLCD_VolatgeTrimNo;SLCD_Init(LCD0, &cfg):把 16 个 WF[](32-bit 视图对应 WF8B[])清零、写入 GCR 控制位、配置背板引脚。
program_backplanes():把 COM0..COM3 绑到相位 A/B/C/D。
SLCD_StartDisplay(LCD0):使能 LCD 驱动开始扫描。
SLCD_Init 已经把所有 WF8B[] 清零,不需要再写一次循环清零。
7. 公共 API
/* slcd_od6010.h */
int slcd_init(void);
void slcd_clear(void);
void slcd_show_number(uint8_t pos, uint8_t num);
void slcd_show_string(const char *str);
void slcd_set_icon(uint8_t icon_id, bool on);
void slcd_show_decimal(int32_t value);
| slcd_init() | 一次性的初始化(详见 §6)。必须在所有其他 slcd_* 之前调用。 |
| slcd_clear() | 清掉所有段和 DP,但控制器继续扫描。 |
| slcd_show_number(pos, num) | 在 pos(0..5,左到右)显示 num(0..9),其他数字不变。 |
| slcd_show_string(str) | 解析最多 6 字符,'0'..'9' 正常显示,其余字符当作空白。 |
| slcd_set_icon(DPx, on) | 点亮/熄灭指定 DP。 |
| slcd_show_decimal(value) | 取 value mod 10^6,以 6 位前面补零的形式显示。 |
驱动内部维护两份影子状态:
static uint8_t s_digit_glyph[SLCD_DIGIT_COUNT]; /* 每位的当前数字 0..9 或 0=空 */
static uint8_t s_dp_glyph[SLCD_ICON_COUNT]; /* 每个 DP 的开关 */
每次修改一个字或一个 DP,只把受影响的那几路 WF8B[] 字节重写一次——这样改 slcd_show_number(2, 5) 不会让其他位的旧显示闪烁。
8. 演示程序:1Hz 计数器
在初始化后做两件事:
开机显示:写 123456,把 6 个 DP 全点亮 2 秒,验证每个位置和每个圆点的引脚映射都正确。然后清屏进入主循环。
1Hz 计数:每秒 counter++ 并 slcd_show_decimal(counter);同时让 DP4/DP5 以 2 Hz 闪烁,DP6 与 counter 的最低位联动(每秒切换一次)。
while (1) {
uint64_t now = k_uptime_get();
if (now - last_count >= COUNT_PERIOD_MS) {
last_count = now;
counter = (counter + 1U) % 1000000U;
slcd_show_decimal((int32_t)counter);
}
if (now - last_blink >= BLINK_PERIOD_MS) {
last_blink = now;
blink_phase ^= 1U;
slcd_set_icon(SLCD_ICON_DP4, blink_phase);
slcd_set_icon(SLCD_ICON_DP5, blink_phase);
slcd_set_icon(SLCD_ICON_DP6,
blink_phase && (counter & 0x1U));
}
k_msleep(10U);
}
9. 构建与烧录
export ZEPHYR_TOOLCHAIN_VARIANT=zephyr
export ZEPHYR_SDK_INSTALL_DIR=/d/luglZephyrproject/zephyr-sdk-1.0.1/zephyr-sdk-1.0.1
# 配置并构建(-p always 强制 cmake 重跑首次构建)
west build -b frdm_mcxa366 app/frdm_mcxa366_slcd -p always
# 增量构建
west build -b frdm_mcxa366 app/frdm_mcxa366_slcd
# 通过 J-Link 烧录
west flash -r jlink
构建产物在 app/frdm_mcxa366_slcd/build/zephyr/zephyr.elf,FLASH 占用约 32 KB / 1 MB,RAM 占用约 6 KB / 240 KB。
10. 文件清单
1、 main.c
/*
* Copyright (c) 2026
*
* SPDX-License-Identifier: Apache-2.0
*
* Demo for the OD-6010 segment-LCD driver on the FRDM-MCXA366 board.
*
* The main thread counts up from 0 once per second and shows the value
* on the six-digit display. Three of the battery icons are toggled to
* blink at 1 Hz so the user can see the icon API in action.
*/
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#include "slcd_od6010.h"
#define BLINK_PERIOD_MS 500U
#define COUNT_PERIOD_MS 1000U
int main(void)
{
int rc;
uint32_t counter = 0U;
uint32_t blink_phase = 0U;
uint64_t last_count = 0U;
uint64_t last_blink = 0U;
printk("FRDM-MCXA366 OD-6010 SLCD demo\n");
printk("6-digit counter + icon blink at %u Hz\n",
1000 / BLINK_PERIOD_MS);
rc = slcd_init();
if (rc != 0) {
printk("slcd_init failed: %d\n", rc);
return rc;
}
/* Sign-on: show "123456" with every DP lit so the user can
* confirm the full dot layout on the OD-6010 glass. The glass
* has six dots in three gaps between the inner digits — upper
* (DP1/2/3, between positions 1-2, 2-3, 3-4) and lower
* (DP4/5/6, in the same three gaps). No dots between the
* outermost gaps (pos 0-1 or pos 4-5).
*/
slcd_show_string("123456");
slcd_set_icon(SLCD_ICON_DP1, true);
slcd_set_icon(SLCD_ICON_DP2, true);
slcd_set_icon(SLCD_ICON_DP3, true);
slcd_set_icon(SLCD_ICON_DP4, true);
slcd_set_icon(SLCD_ICON_DP5, true);
slcd_set_icon(SLCD_ICON_DP6, true);
k_msleep(2000);
slcd_clear();
while (1) {
uint64_t now = k_uptime_get();
/* ----- count once per second ----- */
if (now - last_count >= COUNT_PERIOD_MS) {
last_count = now;
counter = (counter + 1U) % 1000000U;
slcd_show_decimal((int32_t)counter);
}
/* ----- blink DP4..DP6 at 1 Hz ----- */
if (now - last_blink >= BLINK_PERIOD_MS) {
last_blink = now;
blink_phase ^= 1U;
slcd_set_icon(SLCD_ICON_DP4, blink_phase);
slcd_set_icon(SLCD_ICON_DP5, blink_phase);
slcd_set_icon(SLCD_ICON_DP6,
blink_phase && (counter & 0x1U));
}
k_msleep(10U);
}
return 0;
}2、slcd_od6010.c
/*
* Copyright (c) 2026
*
* SPDX-License-Identifier: Apache-2.0
*
* OD-6010 segment-LCD driver — implementation.
*
* See slcd_od6010.h for the public API and the glass-layout description.
* The driver mixes the MCUX HAL (fsl_slcd.h) into the Zephyr build — Zephyr
* does not ship an SLCD devicetree node for the MCXA366. All pin
* routing, duty-cycle, and backplane configuration comes from NXP's
* official frdmmcxa366_slcd reference example
* (board/app.h, board/pin_mux.c, slcd/OD-6010.c).
*/
#include "slcd_od6010.h"
#include <stddef.h>
#include <string.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
/* The MCUX SDK is built with CMSIS headers; the SLCD HAL and PORT mux
* helpers come from the SDK module pulled in via the west.yml entry.
* Zephyr already provides the CMSIS register maps and clock gates, so
* we tell the HAL not to touch them on its own. Guarded in case a
* build-system setting already defined the macro.
*/
#ifndef FSL_SDK_DISABLE_DRIVER_CLOCK_CONTROL
#define FSL_SDK_DISABLE_DRIVER_CLOCK_CONTROL 1
#endif
#include <fsl_slcd.h>
/* The generic MCUX SDK PORT driver is header-only — provides
* PORT_SetPinMux(), PORT_Type and the kPORT_MuxAltN enum. The MCXA366
* device header does not generate its own PERI_PORT.h, so this is the
* canonical entry point.
*/
#include <fsl_port.h>
/* The MCXA366 SDK does not ship its own fsl_clock.h — it inherits the
* one from the MCXA344/MCXA266 family, which already defines the gate
* numbers we need. Including it is sufficient to get kCLOCK_GateSLCD0
* and CLOCK_EnableClock(). We still gate the SLCD peripheral ourselves
* (above) so the function is a no-op here.
*
* Same story for fsl_reset.h: the SLCD reset shift constant
* (kSLCD0_RST_SHIFT_RSTn) lives in the MCXA346/266 driver, not in the
* (empty) MCXA366 driver set. We need RESET_ReleasePeripheralReset()
* to release the SLCD before the first register access — otherwise
* the bus faults because the peripheral is still in reset.
*/
#include <fsl_clock.h>
#include <fsl_reset.h>
/* MCXA366 PORT base addresses — the on-tree SDK has no PERI_PORT.h for
* the MCXA366, so we define the five PORT instances here from the
* reference manual (same layout as MCXA266 / 344 / 346).
*/
#ifndef PORT0_BASE
#define PORT0_BASE (0x400BC000u)
#define PORT1_BASE (0x400BD000u)
#define PORT2_BASE (0x400BE000u)
#define PORT3_BASE (0x400BF000u)
#define PORT4_BASE (0x400C0000u)
#define PORT0 ((PORT_Type *)PORT0_BASE)
#define PORT1 ((PORT_Type *)PORT1_BASE)
#define PORT2 ((PORT_Type *)PORT2_BASE)
#define PORT3 ((PORT_Type *)PORT3_BASE)
#define PORT4 ((PORT_Type *)PORT4_BASE)
#endif
/* --------------------------------------------------------------------------
* SLCD <-> GPIO pin table for the FRDM-MCXA366 board (per NXP example)
*
* The MCXA366 SLCD peripheral exposes 48 pins (P0..P47). On this board
* the OD-6010 glass is wired to pins 16..31, all routed through PORT0
* pads 12..27 in mux ALT9 (LCD function). The table below lists every
* pin we actually drive — the rest stay in their reset (GPIO) state.
*
* slcd_pin role gpio pad board signal
* -------- --------- -------- ----
* 16 SEG1a/b/c/d PORT0[12] P0_12
* 17 SEG1e/f/g PORT0[13] P0_13
* 18 SEG2a/b/c/d PORT0[14] P0_14
* 19 SEG2e/f/g PORT0[15] P0_15
* 20 SEG3a/b/c/d PORT0[16] P0_16
* 21 SEG3e/f/g PORT0[17] P0_17
* 22 SEG4a/b/c/d PORT0[18] P0_18
* 23 SEG4e/f/g PORT0[19] P0_19
* 24 SEG5a/b/c/d PORT0[20] P0_20
* 25 SEG5e/f/g PORT0[21] P0_21
* 26 SEG6a/b/c/d PORT0[22] P0_22
* 27 SEG6e/f/g PORT0[23] P0_23
* 28 COM0 PORT0[24] P0_24
* 29 COM1 PORT0[25] P0_25
* 30 COM2 PORT0[26] P0_26
* 31 COM3 PORT0[27] P0_27
*
* If your board routes the glass differently, edit this table and
* nothing else needs to change.
* ------------------------------------------------------------------------ */
typedef struct {
uint8_t slcd_pin; /* SLCD peripheral pin index (16..31 here) */
uint8_t port; /* GPIO port (0 on MCXA366 for these pads) */
uint8_t gpio_pin; /* GPIO pin (12..27 here) */
} slcd_pin_map_t;
static const slcd_pin_map_t k_pin_map[] = {
/* 12 segment pins */
{16U, 0U, 12U},
{17U, 0U, 13U},
{18U, 0U, 14U},
{19U, 0U, 15U},
{20U, 0U, 16U},
{21U, 0U, 17U},
{22U, 0U, 18U},
{23U, 0U, 19U},
{24U, 0U, 20U},
{25U, 0U, 21U},
{26U, 0U, 22U},
{27U, 0U, 23U},
/* 4 backplane pins */
{28U, 0U, 24U},
{29U, 0U, 25U},
{30U, 0U, 26U},
{31U, 0U, 27U},
};
#define PIN_MAP_LEN (sizeof(k_pin_map) / sizeof(k_pin_map[0]))
/* --------------------------------------------------------------------------
* Glass geometry — derived from the OD-6010 panel definition.
*
* Each digit owns a pair of segment pins (logical "1..12" in NXP's
* terminology). Pin (2x-1) carries segments a, b, c, d on phases
* A, B, C, D respectively. Pin (2x) carries segments e, f, g but on
* the ASYMMETRIC phase set {D, A, C} — that mapping is hard-wired in
* the OD-6010 glass; we cannot change it.
*
* SLCD peripheral pin index = logical pin index + 15, so logical
* pin (2x-1) = SLCD pin (15 + 2x), pin (2x) = SLCD pin (16 + 2x).
* ------------------------------------------------------------------------ */
#define PHASE_A kSLCD_PhaseAActivate /* bit 0 */
#define PHASE_B kSLCD_PhaseBActivate /* bit 1 */
#define PHASE_C kSLCD_PhaseCActivate /* bit 2 */
#define PHASE_D kSLCD_PhaseDActivate /* bit 3 */
/* Per-digit SLCD pin indices (1-indexed logical → 0-indexed SLCD).
*
* Digit x (1..6) owns the logical pin pair (2x-1, 2x). NXP's
* slcd_lcd_gpio_seg_pin[] maps logical pin N to SLCD pin (N+15),
* but the OD-6010 glass numbers its digits right-to-left: physical
* layout is dig6 dig5 dig4 dig3 dig2 dig1 (left to right), so
* logical pin 1 (SLCD pin 16) is the RIGHTMOST digit on the panel.
*
* To make our position 0 (the "leftmost" call site) line up with
* the leftmost digit on the glass, we REVERSE the pin assignment:
*
* PIN_ABCN(x) = SLCD pin 28 - 2x
* PIN_EFG(x) = SLCD pin 29 - 2x
*
* For 0-based digit index d (0..5), with x = d + 1:
* PIN_ABCN(d) = 26 - 2d (SLCD pins 26, 24, 22, 20, 18, 16)
* PIN_EFG(d) = 27 - 2d (SLCD pins 27, 25, 23, 21, 19, 17)
*/
#define PIN_ABCN(x) (uint32_t)(28U - 2U * (x)) /* a, b, c, d pin */
#define PIN_EFG(x) (uint32_t)(29U - 2U * (x)) /* e, f, g pin */
/* 7-segment bit masks. Bit ordering on each SLCD pin matches NXP's
* OD-6010 reference (OD-6010.c: bit0 = phase A, bit3 = phase D, etc.).
*
* On PIN_ABCN: bit0=A_COM, bit1=B_COM, bit2=C_COM, bit3=D_COM.
* Phase mapping: A=a, B=b, C=c, D=d.
*
* On PIN_EFG: bit0=P_COM, bit1=F_COM, bit2=G_COM, bit3=E_COM.
* Phase mapping: P=DP (decimal point), F=f, G=g, E=e.
* The DP sits on phase A (bit 0) of the EFG pin, so
* enabling a DP ORs phase A into the same byte that
* drives segment 'f'. The OD-6010 glass wired by NXP
* doesn't expose 'f' on the DP pin's electrode, so
* the two don't collide visually.
*/
#define SEGA_ABCN (1U << 0)
#define SEGB_ABCN (1U << 1)
#define SEGC_ABCN (1U << 2)
#define SEGD_ABCN (1U << 3)
#define SEGF_EFG (1U << 1) /* upper-left vertical */
#define SEGG_EFG (1U << 2) /* middle horizontal */
#define SEGE_EFG (1U << 3) /* lower-left vertical */
/* Font tables: each digit is described by two phase bytes — one for
* the ABCD pin and one for the EFG pin. Index 0..9 are digits, 10
* is the blank glyph (all segments off). The letter forms (A..F)
* reuse the digit shape with the lowercase 'b'..'f' mapping where the
* panel supports it; we keep this table simple (digits + blank) and
* fall back to blank for any unrendered glyph.
*/
static const uint8_t k_digit_abcn[16] = {
[0] = SEGA_ABCN | SEGB_ABCN | SEGC_ABCN | SEGD_ABCN, /* 0 = ABCD */
[1] = SEGB_ABCN | SEGC_ABCN, /* 1 = BC */
[2] = SEGA_ABCN | SEGB_ABCN | SEGD_ABCN, /* 2 = ABD */
[3] = SEGA_ABCN | SEGB_ABCN | SEGC_ABCN | SEGD_ABCN, /* 3 = ABCD */
[4] = SEGB_ABCN | SEGC_ABCN, /* 4 = BC */
[5] = SEGA_ABCN | SEGC_ABCN | SEGD_ABCN, /* 5 = ACD */
[6] = SEGA_ABCN | SEGC_ABCN | SEGD_ABCN, /* 6 = ACD */
[7] = SEGA_ABCN | SEGB_ABCN | SEGC_ABCN, /* 7 = ABC */
[8] = SEGA_ABCN | SEGB_ABCN | SEGC_ABCN | SEGD_ABCN, /* 8 = ABCD */
[9] = SEGA_ABCN | SEGB_ABCN | SEGC_ABCN | SEGD_ABCN, /* 9 = ABCD */
};
static const uint8_t k_digit_efg[16] = {
[0] = SEGE_EFG | SEGF_EFG, /* 0 = EF */
[1] = 0U, /* 1 = - */
[2] = SEGE_EFG | SEGG_EFG, /* 2 = EG */
[3] = SEGG_EFG, /* 3 = G */
[4] = SEGF_EFG | SEGG_EFG, /* 4 = FG */
[5] = SEGF_EFG | SEGG_EFG, /* 5 = FG */
[6] = SEGE_EFG | SEGF_EFG | SEGG_EFG, /* 6 = EFG */
[7] = 0U, /* 7 = - */
[8] = SEGE_EFG | SEGF_EFG | SEGG_EFG, /* 8 = EFG */
[9] = SEGF_EFG | SEGG_EFG, /* 9 = FG */
};
/* Backplane phase table — only the four phases used by 1/4 duty. */
static const slcd_phase_type_t k_digit_phase[SLCD_DIGIT_COUNT] = {
PHASE_A, PHASE_B, PHASE_C, PHASE_D,
};
/* Back-plane pin index for each of the four COM lines. */
static const uint32_t k_com_pin[4] = {
28U, 29U, 30U, 31U,
};
/* Decimal-point pin index (logical pin 1..12 = SLCD pin 16..27, DP is
* lit on phase A only — see slcd_od6010.h for the mapping).
*
* OD-6010 has six dots in three gaps (paired upper/lower) between the
* four inner digits (dig2..dig5 on the glass). Per the NXP SLCD_Icon[]
* array and the Orient Display OD-6010 datasheet:
* P1 (upper), P4 (lower) — between dig5 and dig4 → SLCD pin 23/25
* P2 (upper), P5 (lower) — between dig4 and dig3 → SLCD pin 21/27
* P3 (upper), P6 (lower) — between dig3 and dig2 → SLCD pin 19/17
* After reversing the position mapping (see PIN_EFG above), the SLCD
* pin formula is PIN_EFG(x) = 29 - 2x, so the digit whose EFG pin this
* DP shares is:
*/
static const uint32_t k_dp_pin[SLCD_ICON_COUNT] = {
/* DP1 */ 23U, /* SLCD P8 — upper, between pos 1–2 (dig5–dig4) */
/* DP2 */ 21U, /* SLCD P6 — upper, between pos 2–3 (dig4–dig3) */
/* DP3 */ 19U, /* SLCD P4 — upper, between pos 3–4 (dig3–dig2) */
/* DP4 */ 25U, /* SLCD P10 — lower, between pos 1–2 (dig5–dig4) */
/* DP5 */ 27U, /* SLCD P12 — lower, between pos 2–3 (dig4–dig3) */
/* DP6 */ 17U, /* SLCD P2 — lower, between pos 3–4 (dig3–dig2) */
};
/* Per-digit shadow of the current digit glyph (0..9, others = blank). */
static uint8_t s_digit_glyph[SLCD_DIGIT_COUNT];
/* Per-DP shadow. DP pins share their phase-A bit with the 'f'
* segment on the same EFG pin, so we OR the DP bit into whatever the
* digit-rendering code wrote. Phase A on PIN_EFG = SEGF_EFG = bit 0.
*/
static uint8_t s_dp_glyph[SLCD_ICON_COUNT];
/* --------------------------------------------------------------------------
* Waveform helpers
* ------------------------------------------------------------------------ */
static void program_backplanes(void)
{
for (uint32_t d = 0; d < 4U; d++) {
SLCD_SetBackPlanePhase(LCD0, k_com_pin[d], k_digit_phase[d]);
}
}
/* Write the per-digit ABCD + EFG phase bytes from the shadow state. */
static void refresh_digit(uint32_t pos)
{
uint8_t glyph = s_digit_glyph[pos];
if (glyph > 9U) {
glyph = 0U; /* treat as blank */
s_digit_glyph[pos] = 0U;
}
SLCD_SetFrontPlaneSegments(LCD0, PIN_ABCN(pos + 1U),
k_digit_abcn[glyph]);
SLCD_SetFrontPlaneSegments(LCD0, PIN_EFG(pos + 1U),
k_digit_efg[glyph]);
}
/* Re-write every digit after the shadow changed. */
static void refresh_all(void)
{
for (uint32_t d = 0; d < SLCD_DIGIT_COUNT; d++) {
refresh_digit(d);
}
}
/* Update a single DP pin by OR-ing the DP phase-A bit into whatever
* the EFG pin currently shows. Called from slcd_set_icon().
*/
static void refresh_dp(uint32_t icon_id);
/* Helper: re-merge DPs that share a digit's EFG pin. Called from
* slcd_show_number() so changing a digit doesn't drop a lit DP.
*/
static void refresh_dp_for_digit(uint32_t pos);
static void refresh_dp(uint32_t icon_id)
{
uint32_t slcd_pin = k_dp_pin[icon_id];
uint8_t efg_phase;
/* The DP shares its SLCD pin with the EFG pin of the digit it
* sits next to (DP1 → PIN_EFG(3) = SLCD pin 23, etc.). We OR
* bit 0 (phase A) onto the existing EFG phase byte if this DP
* is on; otherwise we re-write the pin with just the digit's
* EFG byte (which never sets phase A for digits 0..9, so the
* DP bit really turns off).
*/
uint32_t efg_digit = (27U - slcd_pin) / 2U; /* PIN_EFG(x) = 28-2x */
uint8_t glyph = s_digit_glyph[efg_digit];
if (glyph > 9U) {
glyph = 0U;
}
efg_phase = k_digit_efg[glyph];
if (s_dp_glyph[icon_id] != 0U) {
efg_phase |= (uint8_t)PHASE_A;
}
SLCD_SetFrontPlaneSegments(LCD0, slcd_pin, efg_phase);
}
/* --------------------------------------------------------------------------
* Public API
* ------------------------------------------------------------------------ */
int slcd_init(void)
{
slcd_config_t cfg;
/* Route every SLCD pin we use to its GPIO pad. ALT9 selects the
* LCD function on PORT0_12..PORT0_27 (verified in the MCXA366
* reference manual and the NXP pin_mux.c).
*/
static PORT_Type *const k_ports[5] = {
PORT0, PORT1, PORT2, PORT3, PORT4,
};
for (size_t i = 0; i < PIN_MAP_LEN; i++) {
uint8_t port = k_pin_map[i].port;
if (port >= ARRAY_SIZE(k_ports)) {
continue;
}
PORT_SetPinMux(k_ports[port], k_pin_map[i].gpio_pin,
kPORT_MuxAlt9);
}
/* Ungate the SLCD peripheral clock. Zephyr leaves the gate
* alone (we set FSL_SDK_DISABLE_DRIVER_CLOCK_CONTROL), so we
* have to enable it ourselves.
*
* The SLCD starts in reset out of POR — touching any LCD0
* register while the reset is asserted returns a bus fault.
* Release the reset first, then ungate the clock.
*/
RESET_ReleasePeripheralReset(kSLCD0_RST_SHIFT_RSTn);
CLOCK_EnableClock(kCLOCK_GateSLCD0);
/* Default configuration: 1/4 duty, charge pump off, internal VLL3,
* no fault detection. The MCXA366 has FSL_FEATURE_SLCD_LP_CONTROL
* so the default fills in the LP fields for us; we only override
* the duty cycle and clock prescaler.
*/
SLCD_GetDefaultConfig(&cfg);
cfg.displayMode = kSLCD_NormalMode;
cfg.dutyCycle = kSLCD_1Div4DutyCycle;
cfg.lowPowerBehavior = kSLCD_EnabledInWaitStop;
cfg.lowPowerWaveform = true;
cfg.voltageTrimVLL1 = kSLCD_VolatgeTrimNo;
cfg.voltageTrimVLL2 = kSLCD_VolatgeTrimNo;
cfg.sampleHold = kSLCD_SampleHoldNone;
cfg.clkPrescaler = kSLCD_ClkPrescaler04; /* ~64 Hz @ 32 kHz */
#if FSL_FEATURE_SLCD_HAS_FRAME_FREQUENCY_INTERRUPT
cfg.frameFreqIntEnable = false;
#endif
cfg.faultConfig = NULL;
/* From NXP board/app.h: only pins 16..31 are enabled, and
* pins 28..31 are backplanes. High-half register is unused.
*/
cfg.slcdLowPinEnabled = 0xFFFF0000U;
cfg.slcdHighPinEnabled = 0U;
cfg.backPlaneLowPin = 0xF0000000U;
cfg.backPlaneHighPin = 0U;
SLCD_Init(LCD0, &cfg);
/* SLCD_Init() already clears every WF8B[0..47] byte via the
* WF[] 32-bit access path (it iterates 12 entries covering all
* 48 bytes). Do NOT add a second clear loop here — writes
* past offset 0x4F fault (BFAR Address: 0x400c2054, i.e.
* WF8B[52]).
*/
program_backplanes();
memset(s_digit_glyph, 0, sizeof(s_digit_glyph));
memset(s_dp_glyph, 0, sizeof(s_dp_glyph));
refresh_all();
SLCD_StartDisplay(LCD0);
return 0;
}
void slcd_clear(void)
{
memset(s_digit_glyph, 0, sizeof(s_digit_glyph));
memset(s_dp_glyph, 0, sizeof(s_dp_glyph));
/* Zero every front-plane byte we touch — pins 16..31 (12 SEG +
* 4 COM). Skip the backplane phase bytes we just set.
*/
for (uint32_t i = 16U; i < 28U; i++) {
LCD0->WF8B[i] = 0U;
}
}
void slcd_show_number(uint8_t pos, uint8_t num)
{
if (pos >= SLCD_DIGIT_COUNT) {
return;
}
if (num <= 9U) {
s_digit_glyph[pos] = num;
} else {
s_digit_glyph[pos] = 0U; /* render as blank */
}
refresh_digit(pos);
refresh_dp_for_digit(pos);
}
void slcd_show_string(const char *str)
{
if (str == NULL) {
slcd_clear();
return;
}
for (uint32_t d = 0; d < SLCD_DIGIT_COUNT; d++) {
char c = str[d];
if (c >= '0' && c <= '9') {
s_digit_glyph[d] = (uint8_t)(c - '0');
} else if (c == '-' || c == '_' || c == ' ') {
s_digit_glyph[d] = 0U; /* blank / dash */
} else {
s_digit_glyph[d] = 0U; /* unsupported glyph */
}
}
refresh_all();
}
void slcd_set_icon(uint8_t icon_id, bool on)
{
if (icon_id >= SLCD_ICON_COUNT) {
return;
}
/* The DP pin shares the EFG pin of the digit it sits next to.
* We keep the DP state in s_dp_glyph[] and re-merge it with the
* digit's EFG phase byte whenever either changes. Phase A is
* bit 0 — and the 'f' segment is also bit 0 on the EFG pin, so
* turning a DP on actually lights BOTH 'f' and the DP. The
* OD-6010 glass wired by NXP doesn't expose that 'f' segment as
* visible (it falls outside the 7-seg area), so this overlap
* does not show up on the panel.
*/
s_dp_glyph[icon_id] = on ? 1U : 0U;
refresh_dp(icon_id);
}
/* Helper: re-merge DPs that share a digit's EFG pin. Called from
* slcd_show_number() so changing a digit doesn't drop a lit DP.
*/
static void refresh_dp_for_digit(uint32_t pos)
{
for (uint32_t i = 0; i < SLCD_ICON_COUNT; i++) {
uint32_t slcd_pin = k_dp_pin[i];
uint32_t efg_digit = (27U - slcd_pin) / 2U;
if (efg_digit == pos) {
refresh_dp(i);
}
}
}
void slcd_show_decimal(int32_t value)
{
char buf[SLCD_DIGIT_COUNT + 1];
uint32_t uval;
if (value < 0) {
uval = (uint32_t)(-value);
} else {
uval = (uint32_t)value;
}
/* Take value modulo 10^6 — anything larger doesn't fit. */
uval %= 1000000U;
buf[SLCD_DIGIT_COUNT] = '\0';
for (uint32_t i = SLCD_DIGIT_COUNT; i > 0U; i--) {
buf[i - 1U] = (char)('0' + (uval % 10U));
uval /= 10U;
}
slcd_show_string(buf);
}3、slcd_6010.h
/*
* Copyright (c) 2026
*
* SPDX-License-Identifier: Apache-2.0
*
* Driver for the OD-6010 6-digit 7-segment + decimal-point LCD driven by
* the on-chip SLCD controller of the NXP MCXA366 (used on the FRDM-
* MCXA366 board). Zephyr has no upstream SLCD devicetree node, so this
* driver mixes the MCUX HAL (fsl_slcd.h) into the Zephyr build without
* modifying the kernel.
*
* Glass layout (verified against NXP's official frdmmcxa366_slcd example):
*
* - 6 numeric digits (DIG1..DIG6, left to right)
* - 1/4 duty cycle — 4 backplanes (COM0..COM3)
* - Each digit owns 2 SLCD segment pins:
* pin (2x-1) drives segments a, b, c, d on phases A, B, C, D
* pin (2x) drives segments e, f, g on phases E, F, G
* (the EFG pin also carries the DP on phase A, sharing bit 0)
* - 6 decimal points (DP1..DP6), one per digit, on phase A
* - 12 segment pins total → SLCD pins 16..27 → PORT0 pads 12..23
* - 4 backplane pins → SLCD pins 28..31 → PORT0 pads 24..27
*
* Waveform summary (1/4 duty, phases A..D for COM0..COM3):
* - During phase A: COM0 high, COM1..COM3 low → only segments wired
* to "phase A on their pin" are visible (those would be the 'a'
* segments of every digit and 'f' segments of every digit, plus
* DP1..DP6).
* - A SEG pin lights any segment whose phase bit is set in its
* WF8B[] byte — only segments whose COM is also high become visible.
* - Phases E..H are unused by this glass; their bits always read 0.
*/
#ifndef SLCD_OD6010_H_
#define SLCD_OD6010_H_
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Numeric digit positions, left to right. */
#define SLCD_DIGIT_COUNT 6U
#define SLCD_DIGIT_LEFT 0U
#define SLCD_DIGIT_RIGHT 5U
/* Decimal-point identifiers (passed to slcd_set_icon()).
*
* The OD-6010 glass wired up by the FRDM-MCXA366 has six DPs, one per
* digit. No battery / signal-bar icons are exposed on this board
* variant — see the NXP frdmmcxa366_slcd example for the canonical
* reference.
*/
enum slcd_icon {
SLCD_ICON_DP1 = 0, /* DP for digit 1 (leftmost) */
SLCD_ICON_DP2 = 1,
SLCD_ICON_DP3 = 2,
SLCD_ICON_DP4 = 3,
SLCD_ICON_DP5 = 4,
SLCD_ICON_DP6 = 5, /* DP for digit 6 (rightmost) */
SLCD_ICON_COUNT = 6,
};
/* Initialize the SLCD controller, route the COM/SEG pins to the LCD
* peripheral, and turn on the display. Must be called once at startup
* before any other slcd_*() call.
*
* Returns 0 on success, negative errno on failure.
*/
int slcd_init(void);
/* Turn off every segment and decimal point (display goes blank but the
* controller keeps running).
*/
void slcd_clear(void);
/* Show one decimal digit (0..9) at position pos (0..5, left to right).
* Out-of-range digits are rendered as blanks.
*/
void slcd_show_number(uint8_t pos, uint8_t num);
/* Show a NUL-terminated string of up to 6 characters. Only the ASCII
* subset '0'..'9', 'A'..'F', '-', ' ' is rendered — every other
* character becomes a blank. Trailing characters past the rightmost
* digit are ignored.
*/
void slcd_show_string(const char *str);
/* Turn a decimal point on or off. icon_id is one of enum slcd_icon
* (SLCD_ICON_DP1..SLCD_ICON_DP6). Out-of-range IDs are silently
* ignored.
*/
void slcd_set_icon(uint8_t icon_id, bool on);
/* Convenience: clear and write the decimal representation of `value`.
* `value` is rendered modulo 10^6 (so 1234567 displays as "234567").
*/
void slcd_show_decimal(int32_t value);
#ifdef __cplusplus
}
#endif
#endif /* SLCD_OD6010_H_ */4、CMakeLists.txt
# SPDX-License-Identifier: Apache-2.0
#
# Build the OD-6010 SLCD demo for the FRDM-MCXA366 board.
#
# This sample mixes the MCUX HAL (fsl_slcd.h, fsl_port.h, fsl_clock.h) into
# the Zephyr application. Zephyr's own pinctrl/GPIO drivers stay out of
# the SLCD pins — we route them through the MCUX PORT mux helper inside
# slcd_od6010.c.
#
# Required pieces:
# * The NXP HAL west module must be present (modules/hal/nxp/) so that
# `fsl_slcd.c` is available. Adding mcux-sdk-ng as a Zephyr module
# via `west.yml` provides it.
# * The MCXA366 device headers must be on the include path so that
# `MCXA366.h` (and through it `fsl_device_registers.h`) can be found.
# We add the relevant paths below.
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(frdm_mcxa366_slcd)
# --------------------------------------------------------------------------
# MCUX HAL include paths
#
# The NXP HAL west module ships device headers under:
# modules/hal/nxp/mcux/mcux-sdk-ng/devices/MCX/MCXA/MCXA366/
# and shared utilities / drivers under:
# modules/hal/nxp/mcux/mcux-sdk-ng/
# modules/hal/nxp/mcux/mcux-sdk-ng/utilities/
# modules/hal/nxp/mcux/mcux-sdk-ng/drivers/slcd/
# modules/hal/nxp/mcux/mcux-sdk-ng/drivers/port/
# modules/hal/nxp/mcux/mcux-sdk-ng/components/...
# We add them to the app's include path so `#include <fsl_slcd.h>` and
# friends resolve. Zephyr itself does not include any of these.
# --------------------------------------------------------------------------
set(MCUX_SDK_NG_ROOT "${ZEPHYR_BASE}/../modules/hal/nxp/mcux/mcux-sdk-ng")
zephyr_include_directories(
# Device header for MCXA366 (provides MCXA366.h, MCXA366_COMMON.h,
# PERI_LCD.h, and the LCD0_Type typedef).
${MCUX_SDK_NG_ROOT}/devices/MCX/MCXA/MCXA366
# Generic MCUX SDK headers (fsl_common.h, fsl_port.h, etc.).
# fsl_clock.h and the clock-IP gate enum (kCLOCK_GateSLCD0 et al.)
# are pulled in automatically by Zephyr's hal_nxp module for the
# MCXA366, so we do not need to add MCXA266/drivers here.
${MCUX_SDK_NG_ROOT}
${MCUX_SDK_NG_ROOT}/drivers/slcd
${MCUX_SDK_NG_ROOT}/drivers/port
${MCUX_SDK_NG_ROOT}/utilities
${MCUX_SDK_NG_ROOT}/components/clock
)
# Compile the SLCD driver from the MCUX SDK — Zephyr does not build any
# SLCD driver for the MCXA366 itself.
zephyr_library_sources(${MCUX_SDK_NG_ROOT}/drivers/slcd/fsl_slcd.c)
# fsl_clock.h and fsl_port.h are header-only — `CLOCK_EnableClock`,
# `PORT_SetPinMux`, and `kCLOCK_GateSLCD0` are already provided by
# Zephyr's hal_nxp module (which pulls the MCXA346 SoC driver set).
# Do NOT compile MCXA266/devices/MCX/MCXA/MCXA266/drivers/fsl_clock.c
# here — it conflicts with the hal_nxp module's copy.
target_sources(app PRIVATE src/main.c src/slcd_od6010.c)5、prj.conf
# OD-6010 SLCD demo on FRDM-MCXA366 # # This config enables a minimal Zephyr setup that mixes the MCUX HAL # (fsl_slcd.h, fsl_port.h, fsl_clock.h) into the build. No SLCD Kconfig # symbols exist in upstream Zephyr for the MCXA366, so nothing related to # the driver itself needs to be toggled here. CONFIG_GPIO=y # Console + printk for the demo (debug UART on lpuart2 @115200). CONFIG_SERIAL=y CONFIG_CONSOLE=y CONFIG_PRINTK=y CONFIG_UART_CONSOLE=y # main thread needs k_msleep()/k_uptime_get() CONFIG_SYS_CLOCK_EXISTS=y # Pull in the MCUX HAL device header from the NXP module. Zephyr does # not expose the MCXA366 device headers through Kconfig; we add the # include path directly from CMakeLists.txt. No Kconfig entry needed # here, but it is useful to know that the device header ends up in # modules/hal/nxp/mcux/mcux-sdk-ng/devices/MCX/MCXA/MCXA366/. # Reduce firmware size: no logging / no shell / no extra init hooks. CONFIG_LOG=y # No Zephyr SLCD subsystem symbols required.

我要赚赏金
