Funpack-5-1
Funpack-5-1
大家好,我是代码小A
非常荣幸参加由电子森林所举办的Funpack-5-1活动
项目介绍:

目前已实现任务1的基础题目和进阶题目,并顺带完成任务2的基础题目
简短的所有使用到的硬件介绍
所使用的硬件平台如下:
该开发板是由电子森林所提供的NXP(恩智浦)FRDM MCX A346,具体参数如下:
- MCX A346 Arm® Cortex®-M33内核,运行频率高达180MHz,1MB闪存,256KB RAM,带8KB的纠错码(ECC)
- 拥有双FlexPWM、4组16位ADC、专用MAU数学加速器以及SmartDMA
- 高速通用串行总线(HS USB)Type-C连接器(板载MCU-Link调试器),支持CAN/I3C/SPI/I²C/UART连接器(Arduino、PMOD/mikroBUS、DNP)
- 带有CMSIS-DAP的板载MCU-Link调试器 ,JTAG/SWD连接器
- 兼容Arduino、mikroBUS、Pmod多种生态
- 适合:工业自动化、电机控制、IoT边缘计算
所用外设
MCUXpresso IDE配置图:
本项目在 NXP MCXA346 (FRDM-MCXA153) 开发板上实现了一个功能丰富的交互式 Shell (OS-Less)。系统主要使用了以下内部外设来实现核心功能:
1. LPUART (低功耗通用异步收发器)
- 用途:实现开发板与 PC(或网页 Dashboard)之间的命令行通信。
- 特性应用:
- 非阻塞发送 (Non-blocking TX):使用了中断或 DMA 方式发送数据,配合自定义的
usart_print函数,确保在输出大量彩色字符画和日志时不会卡死主循环。 - 环形缓冲区接收 (RingBuffer RX):使用了
LPUART_TransferStartRingBuffer。在后台自动将接收到的字符存入g_rxRingBuffer,主循环中高效轮询提取。这使得 Shell 能够完美支持复杂的 ANSI 转义序列(如左右方向键移动光标、历史记录上下翻页等)。
- 非阻塞发送 (Non-blocking TX):使用了中断或 DMA 方式发送数据,配合自定义的
2. CTIMER (标准计数器/定时器)

- 用途:实现 RGB LED 的硬件级无极调光(PWM 输出)。
- 特性应用:
- 多通道 PWM:使用了
CTIMER2(取决于具体引脚映射)。将 Match 2 通道(CTIMER_MAT_PWM_PERIOD)配置为产生 20kHz 的基础周期。 - 独立占空比控制:使用
CTIMER_MAT_OUT_RED(Match 0),CTIMER_MAT_OUT_GREEN(Match 1),CTIMER_MAT_OUT_BLUE(Match 3) 三个独立通道,分别映射到板载的 R、G、B 灯上。通过更新各通道的脉冲宽度,实现了 0-255 级的色彩调节。
- 多通道 PWM:使用了
3. LPADC (低功耗模数转换器)
- 用途:读取芯片内部的温度传感器,并提取底层模拟电压数据。
- 特性应用:
- 内部通道复用:连接到了 ADC 的通道 26 (
DEMO_LPADC_TEMP_SENS_CHANNEL),这是专用于测量 Core Temperature 的隐藏通道。 - 硬件平均 (Hardware Averaging):配置了 128 次硬件平均 (
kLPADC_HardwareAverageCount128),有效过滤了高速运行时的电气噪声。 - 中断与校准:启用了自动偏移和增益校准。采用了软件触发 (Software Trigger) + 中断服务 (
ADC0_IRQHandler) 的方式。在读取温度时,通过双电流法(获取 $V_{be1}$ 和 $V_{be8}$)和官方非线性补偿算法计算出精确的结温。
- 内部通道复用:连接到了 ADC 的通道 26 (
4. SysTick (系统滴答定时器)
- 用途:提供系统级的心跳节拍,用于软件 RTC (实时时钟) 的更新。
- 特性应用:
- 1ms 周期中断:配置为每秒触发 1000 次中断。在
SysTick_Handler中进行计数,满 1000 次后触发一次秒级更新,进而驱动自定义的rtc_time_t结构体(年月日时分秒)进位,为系统提供了独立于主循环的时间基准。
- 1ms 周期中断:配置为每秒触发 1000 次中断。在
5. GPIO / Port 模块 (间接使用)
- 用途:外设引脚的物理映射。
- 特性应用:通过底层的
BOARD_InitHardware()(通常包含pin_mux.c),将 UART 信号映射到了 USB-Serial 桥接芯片的引脚上,将 CTIMER 的 PWM 信号映射到了驱动板载 RGB LED 的引脚上。
方案框图和项目设计思路介绍
流程框图

设计思路与关键代码介绍
总体流程图

shell字符处理流程图

1. 底层输出架构:变参解析与“半异步”发送
/* Custom Printf */
void usart_print(const char* format, ...) {
static char print_buf[256];
va_list arg;
va_start(arg, format);
int len = vsnprintf(print_buf, sizeof(print_buf), format, arg);
va_end(arg);
if (len > 0) {
xfer_tx.data = (uint8_t*)print_buf;
xfer_tx.dataSize = len;
txOnGoing = true;
LPUART_TransferSendNonBlocking(DEMO_LPUART, &g_lpuartHandle, &xfer_tx);
while (txOnGoing); // 等待发送完成
}
}思路讲解:
这是整个 Shell 的输出基石。为了摆脱标准库 printf 庞大的开销和可能存在的不可控阻塞,这里手写了一个变参打印函数。
- 变参处理: 通过
<stdarg.h>的va_list宏结合vsnprintf,将格式化的字符串(如%d,%s)安全地打包进print_buf中。 - 发送机制: 这里很有意思。调用了 NXP fsl_lpuart 库的非阻塞发送函数
LPUART_TransferSendNonBlocking,但在紧接着的下一行使用了while (txOnGoing);死等。这实际上把异步变成了同步阻塞。 - 探讨: 这样写的好处是逻辑极其简单,绝对不会出现缓冲区覆盖(比如上一帧还没发完,下一帧就把
print_buf冲刷了)。但在极高频打印时会占用 CPU 时间。如果后续想做纯异步,可以考虑引入发送队列(Tx FIFO)。
2. 核心外设玩法:基于双 Vbe 电压的温度计算
/* 官方的温度测量辅助函数 */
float DEMO_MeasureTemperature(ADC_Type *base, uint32_t commandId, uint32_t index)
{
// ... 前置初始化省略 ...
if (true == LPADC_GetConvResult(base, &convResultStruct))
{
Vbe1 = convResultStruct.convValue >> convResultShift;
if (true == LPADC_GetConvResult(base, &convResultStruct))
{
Vbe8 = convResultStruct.convValue >> convResultShift;
// 将原始 ADC 值转换为真实电压 (假设 VDDA = 3.3V)
g_Temp_Vbe1_Voltage = ((float)Vbe1 * 3.3f) / 65536.0f;
g_Temp_Vbe8_Voltage = ((float)Vbe8 * 3.3f) / 65536.0f;
temperature = parameterSlope * (parameterAlpha * ((float)Vbe8 - (float)Vbe1) /
((float)Vbe8 + parameterAlpha * ((float)Vbe8 - (float)Vbe1))) -
parameterOffset;
}
}
return temperature;
}思路讲解:
这段代码展示了 MCX 系列 LPADC 获取内部温度的经典原理。MCU 内部的温度传感器通常不是直接输出一个与温度成绝对正比的电压,而是利用三极管基极-发射极电压(Vbe)的温度特性。
- 硬件原理: 芯片内部会向同一个 PN 结分别注入 $1\times$ 和 $8\times$ 的电流,从而产生两个不同的电压
Vbe1和Vbe8。这两个电压的差值($\Delta V_{be}$)与绝对温度成严格的线性比例关系。 - 代码实现: 代码连续读取两次 ADC 转换结果(分别对应 1x 和 8x 电流下的 ADC 值)。你在这里还加了一层非常直观的物理量映射,将 16-bit(65536)的 ADC 值还原成了真实的 3.3V 参考电压下的模拟值。最后套用 NXP 官方校准好的
parameterSlope和parameterOffset公式算出摄氏度。
3. 交互灵魂:VT100 终端转义序列的状态机
/* Input Processor */
void shell_input_handler(uint8_t ch) {
static int escState = 0;
// ANSI Sequence Logic
if (escState == 0 && ch == 0x1B) { escState = 1; return; }
if (escState == 1) { escState = (ch == '[') ? 2 : 0; return; }
if (escState == 2) {
if (ch == 'A' || ch == 'B') {
// ... 处理上下方向键(历史记录) ...
}
else if (ch == 'D') { // Left
if (g_shellPos > 0) { g_shellPos--; usart_print("\b"); }
}
else if (ch == 'C') { // Right
if (g_shellPos < g_shellLen) {
char s[2] = {g_shellBuffer[g_shellPos], 0}; usart_print(s); g_shellPos++;
}
}
escState = 0; return;
}
// ... 处理回车、退格、常规字符 ...
}思路讲解:
这是让这个 Shell 具有“现代感”的核心。当你在键盘上按下方向键(↑、↓、←、→)时,串口发出的不是单个字符,而是 3 个字节的 ANSI 转义序列(例如左方向键是 0x1B 0x5B 0x44,即 ESC [ D)。
- 状态机设计: 使用了一个极其巧妙的轻量级状态机
escState。- 遇到
0x1B(ESC),进入状态 1,拦截该字符不上屏。 - 遇到
[,进入状态 2,准备接收方向指令。 - 遇到
A/B/C/D,执行对应的光标移动或历史记录拉取逻辑,然后状态机复位。
- 遇到
- 光标控制: 比如向左移动光标(
ch == 'D'),内部逻辑不仅把缓冲区的游标g_shellPos减一,还通过发送\b(退格符)让终端上的光标真实地向左退一格。这种软硬件同步的设计非常扎实。
4. 裸机调度核心:RingBuffer 与主循环轮询
int main(void) {
// ... 硬件、定时器、ADC 初始化省略 ...
// Start RingBuffer Background Receiving
LPUART_TransferStartRingBuffer(DEMO_LPUART, &g_lpuartHandle, g_rxRingBuffer, RX_RING_BUFFER_SIZE);
while (1) {
size_t ringLen = LPUART_TransferGetRxRingBufferLength(DEMO_LPUART, &g_lpuartHandle);
if (ringLen > 0) {
size_t toRead = (ringLen > sizeof(g_tempRxBuffer)) ? sizeof(g_tempRxBuffer) : ringLen;
rxXfer.data = g_tempRxBuffer;
rxXfer.dataSize = toRead;
// 从 RingBuffer 提取数据到临时 Buffer
LPUART_TransferReceiveNonBlocking(DEMO_LPUART, &g_lpuartHandle, &rxXfer, &receivedBytes);
for (size_t i = 0; i < receivedBytes; i++) {
shell_input_handler(g_tempRxBuffer[i]);
}
}
}
}思路讲解:
在一个没有 RTOS 的系统中,如何保证用户狂按键盘时数据不丢失?这里的答案是:底层中断接收 + 上层轮询处理。
- 解耦机制:
LPUART_TransferStartRingBuffer是 NXP HAL 库提供的一个非常实用的 API。它在底层开启了 RX 中断,把收到的每一个字节悄悄塞进g_rxRingBuffer中。这个过程是纯硬件/中断驱动的,不会打断主循环太久。 - 集中处理: 在
while(1)死循环中,系统不断检查 RingBuffer 里有没有未读的数据。如果有,就按块(toRead)取出来放进g_tempRxBuffer,然后再逐字节喂给刚才提到的shell_input_handler状态机。 - 优势: 把“耗时的数据解析”从中断服务函数(ISR)中剥离了出来,保证了系统的实时性,同时也避免了频繁的单字节中断读取开销。
调试软件介绍
开发所使用的电脑系统是Ubuntu24.04LTS
所使用的开发软件有:
- MCUXpressoIDE:NXP官方开发工具,通过它编写代码,使用SDK,烧录程序
- vscode:用于编辑代码
- picocom:用于演示为单片机编写的shell
使用到的AI平台有Gemini,ChatGPT, 通义千问
功能展示图及说明
上电打印
我使用的操作系统为Ubuntu24.04LTS
使用命令picocom -b 115200 /dev/ttyACM0打开串口后按下复位按键,效果如下
依次打印:
- LOGO:ACode 和
- Hello, Digikey Funpack 5-1,
- 软件相关的信息,比如名称,作者,版本,构建时间
- 硬件名称,架构,RAM,Flash
- 显示当前状态
- 最后进入shell,未登陆前为访客模式,无法使用全部命令,比如控制RGB等
命令使用
使用help可以查看当前权限下所有可以使用的命令,使用login adin 123456可以登陆获取全部命令的访问权限
使用命令time查看当前系统时间,使用time [set Y M..]设置时间
使用led r g b可以改变rgb亮度

使用命名temp查看芯片温度,同时会显示用于测量温度的两个电压
使用命令pinout查看开发板橄榄图和j1,j2,j3,j4的引脚内容(参考的树莓派的pinout命令)
网页智能终端
该网页终端包含所有命令,可以通过点击或拖拉控制外设,同时预设了多种rbg的显示状态:闪烁,呼吸灯,彩虹呼吸灯
项目中遇到的难题及解决方法
难题1:第一次接触NXP系列的mcu, 这个软件上手还是有点难的,网上教程太少!
解决方法:先看电子森林官方和NXP官方发的上手文档,跑出来一个再慢慢探索,多尝试!多尝试!多尝试!
难题2:代码难读懂,不会使用配置工具配置引脚等
解决方法:先运行官方SDK,在官方SDK的基础上修改,一步一步阅读代码,不懂的就去问AI
心得体会
感谢电子森林举办的这次活动,让我第一次接触到了 NXP 系列的 MCU,感觉这个开发板设计的非常精致,尤其是它“外圈兼容 Arduino,内圈全引脚扩展”的双排针设计,不仅生态好,还没浪费这颗强劲芯片的 IO 资源。官方的 MCUXpresso IDE 用起来也非常舒服,配合图形化配置工具,让我省去了很多手撕底层寄存器的烦恼。通过这个项目,我算是真正摸到了 NXP SDK 开发的门道。
除了工具上的体验,这次在代码设计上也让我大呼过瘾。以前搞单片机,基本上就是写个 while(1) 死循环,然后用 delay 延时,遇到串口通信也是傻等数据。但这次为了搞一个“不卡死”的交互式 Shell,我硬是逼着自己去理解了**环形缓冲区(RingBuffer)和状态机(FSM)**的概念。当我在串口终端里第一次通过按键盘的“上下键”翻出历史记录,还能用左右键移动光标修改命令时,那种“卧槽,裸机被我玩成了 Linux”的成就感真的无以言表。
中间当然也踩了不少坑,最深刻的就是读芯片内部温度。我一开始天真地以为只要复制SDK代码到我项目里就行,结果算出来一个 -1300℃ 的离谱温度。后来详细官方 SDK 源码才恍然大悟:原来 MCU 内部是用“双电流源产生压差”的物理机制搭配中断来测温的,甚至底层还带了一套复杂的非线性补偿算法。这件事让我彻底明白了原厂 BSP/HAL 库存在的意义——它们把极其复杂的物理和模拟特性,优雅地封装成了我们能轻松调用的接口。
最后,把单片机和纯前端网页(Web Serial API)结合在一起,可以说是这次折腾的“点睛之笔”。以前总觉得硬件就是干巴巴的黑框框代码,现在发现只要思路打开,用最简单的 HTML 和 JS 就能给单片机做个炫酷的上位机,连后端服务器都不用搭。滑块一拖,板子上的 RGB 灯跟着无延迟变色,这种从前端 UI 一路打通到底层硬件的全栈开发体验,真的太奇妙了!
总的来说,这次不仅是学了一块新板子,更是把自己的嵌入式编程思维从“单线思维”升级到了“事件驱动和软硬解耦”的高度,收获巨大,期待以后 电子森林 折腾出更多好玩的项目!