Funpack5-1 基于FRDM-MCXA346的串口shell系统设计
该项目使用了MCXA346,实现了串口shell系统的设计,它的主要功能为:通过串口指令控制LED。
标签
Shell
MCXA346
Funpack5-1
孤独的单行者
更新2026-03-13
11

一、任务介绍与项目描述

本项目基于NXP MCXA346微控制器开发板,实现了一个交互式的RGB LED控制系统。该系统通过硬件PWM技术实现对三色LED的精确亮度控制,并提供丰富的可视化效果,包括呼吸灯、闪烁模式和跑马灯等。系统采用Shell命令行交互方式,用户可以通过串口输入命令灵活控制LED的各种工作模式。开发板选用NXP MCXA346作为主控芯片,该芯片基于ARM Cortex-M33内核,运行频率高达180MHz,具备丰富的外设资源,特别是CTimer定时器模块,非常适合用于生成高精度PWM信号。RGB LED采用共阳极接法,通过三路PWM信号分别控制红、绿、蓝三个LED的亮度,混合后即可呈现丰富多彩的颜色效果。

基本功能如下:

blink模式:闪烁模式闪烁频率分别为0.5hz,1hz,2hz;

break模式:呼吸频率也是为0.5hz,1hz,2hz;

跑马灯模式:主要是红绿蓝三色循环以及彩虹色随机模式。

命令分为三段第一段是对应控制LED,第二段控制的内容(模式选择),比如控制模式为mode(闪烁模式、呼吸模式、跑马灯模式),第三段是具体控制指令。

二、硬件介绍

我们使用的硬件实际上主要就是这次用到的目标开发板,FRDM-MCXA346是NXP提供的一款基于MCXA346的紧凑型开发板,具备灵活的扩展性和丰富的硬件接口,适合快速进行原型设计和开发。开发板集成了多个接口模块,使得硬件设计和外设连接更加简便。MCXA346具有强大处理能力的 MCU,支持多种通信协议,具备低功耗特点,适合各种嵌入式应用。开发板提供了丰富的外设接口,包括UART、SPI、I2C 等,方便用户根据不同应用需求进行扩展。板载MCU-Link调试器,支持直接调试MCU程序,支持虚拟串口。开发板上具有板载的RGBLED。

三、方案设计

本次的方案最终要的内容是实现shell功能,实际上就是字符串的命令的解析,本次通过串口的数据解读实现具体命令,我们都知道,创口实际上是发送的八位的数据,通过前后的数据连通可以组成相应的字符串,这些字符串就可以让我们理解更多的命令。shell功能的实现实际上就是通过一种接口实现交互控制方案。

本次大设计主要需要用到的开发板内容包括:

shell交互:串口;

LED控制:PWM+定时器

对于一些外设我们需要关心一些连接问题,比如串口引脚的选择,查看与MCUlink连接的引脚可以看到:


0

上图可以看到,对应的串口可以是LPUART2;

RGBLED的控制引脚如下:


0

如果我们需要用到PWM的控制实际上的通道对应的是CT2的MAT0、MAT1和MAT3。RGB LED与MCXA346开发板的连接采用以下映射关系:

LED颜色

GPIO引脚

定时器通道

功能说明

红色

P3_18

CT2_MAT0

PWM输出通道0

绿色

P3_19

CT2_MAT1

PWM输出通道1

蓝色

P3_21

CT2_MAT3

PWM输出通道3

所有引脚均配置为ALT4功能模式,即CTimer2的匹配输出功能。在引脚初始化过程中,首先使能PORT3的时钟门控,然后配置相应引脚的复用模式为CTimer输出,同时关闭内部上下拉电阻,确保PWM信号能够准确传输到LED驱动电路。

同时考虑到人眼对闪烁的感知特性以及LED的响应速度,本项目将PWM频率设定为20kHz。这一频率远高于人眼的临界闪烁融合频率(约60Hz),能够有效消除人眼可感知的闪烁现象,提供平滑的亮度调节效果。PWM周期值的计算基于CTimer2的时钟频率。MCXA346的FRO_HF时钟源提供180MHz主频,经过1分频后作为CTimer2的时钟输入。PWM周期值计算公式为:周期值 = 系统时钟频率 / PWM频率 - 1。代入数值得出:周期值 = 180000000 / 20000 - 1 = 8999。

四、软件设计

1、单一功能的实现

我们这里用的基础程序是frdmmcxa346_hello_world,所以串口方面并没有太多的地方,工程中已经实现了基本的串口功能,单一功能的实现主要是根据效果实现的需求,进行相关PWM和定时器的实现,这里我们是基于PWM的demo进行的学习实现。

本项目充分利用MCXA346的多定时器资源,采用双定时器协同工作的架构设计,CTimer1作为系统心跳定时器,配置为1ms中断周期。中断服务程序负责递增系统节拍计数器,并调用LED状态更新函数。这种设计将精确的时序控制与LED效果计算分离,确保视觉效果的时间基准稳定可靠。CTimer1的时钟经过180分频后得到1MHz工作频率,匹配寄存器值设为999,实现1ms的定时周期。CTimer2专用于PWM信号生成,采用20kHz频率输出三路独立占空比的PWM波形。MCXA346的CTimer模块支持PWM模式3,在该模式下计数器启动时输出高电平,当计数值与匹配寄存器值相等时切换为低电平。通过设置不同的匹配寄存器值,可以灵活控制每个PWM周期内的高电平持续时间,从而实现0%到100%的占空比调节范围。

PWM初始化过程首先对CTimer2执行软件复位,确保寄存器处于已知状态。随后配置时钟分频器和时钟源,将180MHz的FRO_HF时钟直接馈入CTimer2,不进行额外分频。初始化完成后,调用SDK提供的CTIMER_SetupPwm函数分别配置三个PWM输出通道。在配置过程中,指定MR2寄存器作为PWM周期寄存器,当计数器值与MR2相等时计数器复位并重新开始计数。MR0、MR1、MR3寄存器分别作为红、绿、蓝三色LED的占空比控制寄存器。通过CTIMER_UpdatePwmDutycycle函数可以在程序运行期间动态更新各通道的占空比,而不会影响PWM信号的频率稳定性。

2、目标功能实现

Shell交互模块:Shell模块基于串口中断实现非阻塞字符接收,支持标准的命令行交互模式。用户输入的命令经过解析后分发到对应的处理函数。系统支持以下核心命令:

"LED blink "命令启动闪烁模式,LED以指定频率在完全亮起和完全熄灭之间切换。支持0.5Hz、1Hz和2Hz三种频率选项,闪烁效果通过周期性切换占空比实现,亮起时占空比为100%,熄灭时为0%。

"LED breath "命令启动呼吸灯模式,LED亮度按照正弦曲线平滑变化,呈现出如同呼吸般的渐明渐暗效果。呼吸周期同样可配置为2秒、1秒或0.5秒,对应0.5Hz、1Hz和2Hz频率。软件内部维护一个256级的相位计数器,配合预计算的亮度查找表实现高效的呼吸效果计算。

"LED marquee rgb"命令启动三色跑马灯模式,LED按照红、绿、蓝的顺序循环显示纯色效果。每个颜色保持500毫秒后自动切换到下一颜色,形成经典的红绿蓝循环动画效果。

"LED marquee random"命令启动随机颜色模式,系统利用伪随机数生成器产生随机色相值,通过HSV到RGB颜色空间转换,输出高饱和度的彩虹色系随机颜色。随机颜色的切换间隔同样为500毫秒,既保证了视觉变化的丰富性,又避免了过于频繁的颜色跳跃。

"stop"命令立即停止所有LED动画效果,并将三色LED的占空比同时置零,使LED完全熄灭。

"help"命令可以获取对应的支持命令的规范格式。

PWM占空比控制:占空比参数的有效范围为0%到100%,其中0%对应LED最亮状态,100%对应LED熄灭状态。这是因为RGB LED采用共阳极接法,PWM输出低电平时LED导通发光,输出高电平时LED截止熄灭。在占空比更新函数中,首先对输入参数进行边界检查,确保不超过有效范围。随后调用SDK的CTIMER_UpdatePwmDutycycle函数,分别更新三个PWM通道的匹配寄存器值。这块是主要的LED控制函数,基本代码如下:

void LED_SetDutyCycle(uint8_t redDuty, uint8_t greenDuty, uint8_t blueDuty)
{
/* 限制范围 0-100 */
if (redDuty > 100) redDuty = 100;
if (greenDuty > 100) greenDuty = 100;
if (blueDuty > 100) blueDuty = 100;

/* 使用SDK API更新占空比 */
CTIMER_UpdatePwmDutycycle(CTIMER2, kCTIMER_Match_2, kCTIMER_Match_0, 100-redDuty);
CTIMER_UpdatePwmDutycycle(CTIMER2, kCTIMER_Match_2, kCTIMER_Match_1, 100-greenDuty);
CTIMER_UpdatePwmDutycycle(CTIMER2, kCTIMER_Match_2, kCTIMER_Match_3, 100-blueDuty);
}

3、软件流程图


0

我们这里主要实现的内容包括单片机基本功能的初始化,最重要的shell模块的初始化,然后是通过中断接收相关数据,但确认命令接收结束后进行执行的解析处理,最后反馈到LED的控制上去。

4、主要代码介绍

我们本次主要进行的实际上就是shell功能的实现,并进行独立模块的设计shell.c和shell.h。

创建了一个shell的缓冲区用于存储接收到指令信息:

/* Shell输入缓冲区 */
static char g_shell_buffer[SHELL_BUFFER_SIZE];
static uint32_t g_buffer_index = 0;

/* Shell命令解析结果 */
typedef struct {
char cmd_str[SHELL_BUFFER_SIZE]; /* 原始命令字符串 */
int argc; /* 参数个数 */
char *argv[SHELL_MAX_ARGS]; /* 参数指针数组 */
} shell_cmd_t;

shell的初始化主要是对一些状态和引导内容进行输出和控制:

void Shell_Init(void)
{
g_buffer_index = 0;
memset(g_shell_buffer, 0, sizeof(g_shell_buffer));

/* 初始化LED */
LED_Init();

PRINTF("\r\n");
PRINTF("========================================\r\n");
PRINTF(" NXP MCXA346 Interactive Shell\r\n");
PRINTF("========================================\r\n");
PRINTF("Type 'help' for available commands.\r\n");
Shell_Prompt();
}

shell主循环处理主要是对命令的回显功能并且处理一些特殊字符:


void Shell_Process(void)
{
char ch;

/* 检查是否有字符输入 - 使用底层UART的非阻塞读取 */
if (UART_TryGetchar(&ch) != kStatus_Success)
{
return; /* 无输入 */
}

/* 处理退格键 (BS: 0x08 或 DEL: 0x7F) */
if (ch == '\b' || ch == 0x7F)
{
if (g_buffer_index > 0)
{
g_buffer_index--;
g_shell_buffer[g_buffer_index] = '\0';
PRINTF("\b \b"); /* 退格、擦除、退格 */
}
return;
}

/* 回显字符 */
PUTCHAR(ch);

/* 处理换行符 - 命令结束 */
if (ch == '\r')
{
/* 先输出换行 */
PRINTF("\r\n");

/* 解析并执行命令 */
if (g_buffer_index > 0)
{
shell_cmd_t cmd;
memset(&cmd, 0, sizeof(shell_cmd_t));
int result = Shell_ParseCommand(g_shell_buffer, &cmd);

if (result == 0)
{
Shell_ExecuteCommand(&cmd);
}
else if (result == -1)
{
PRINTF("Error: Too many arguments.\r\n");
}
else
{
PRINTF("Error: Command parsing failed.\r\n");
}
}

/* 清空缓冲区 */
g_buffer_index = 0;
memset(g_shell_buffer, 0, sizeof(g_shell_buffer));

/* 丢弃可能的换行符 \n (Windows \r\n) */
while (UART_TryGetchar(&ch) == kStatus_Success)
{
if (ch == '\n')
{
break;
}
}

/* 显示新提示符 */
Shell_Prompt();
return;
}

/* 忽略换行符 \n */
if (ch == '\n')
{
return;
}

/* 处理普通字符 - 放入缓冲区 */
if (g_buffer_index < (SHELL_BUFFER_SIZE - 1))
{
g_shell_buffer[g_buffer_index++] = ch;
g_shell_buffer[g_buffer_index] = '\0';
}
else
{
/* 缓冲区满 */
PRINTF("\r\nError: Input buffer overflow!\r\n");
g_buffer_index = 0;
memset(g_shell_buffer, 0, sizeof(g_shell_buffer));
Shell_Prompt();
}
}

然后进行对命令的解析,根据规则得到解析结果:

static int Shell_ParseCommand(char *cmd_str, shell_cmd_t *cmd)
{
char *token;
char *save_ptr;
int argc = 0;
char *p;

/* 初始化argv数组为NULL */
for (int i = 0; i < SHELL_MAX_ARGS; i++)
{
cmd->argv[i] = NULL;
}

/* 保存原始命令到副本 */
strncpy(cmd->cmd_str, cmd_str, SHELL_BUFFER_SIZE - 1);
cmd->cmd_str[SHELL_BUFFER_SIZE - 1] = '\0';

/* 清理字符串:移除所有非打印字符(\r, \n等) */
p = cmd->cmd_str;
while (*p)
{
if ((unsigned char)*p >= 32) /* 只保留可打印字符和空格 */
{
p++;
}
else
{
*p = '\0'; /* 截断字符串 */
break;
}
}

/* 分割命令字符串 */
token = strtok_r(cmd->cmd_str, " \t", &save_ptr);

while (token != NULL && argc < SHELL_MAX_ARGS)
{
cmd->argv[argc++] = token;
token = strtok_r(NULL, " \t", &save_ptr);
}

if (token != NULL && argc >= SHELL_MAX_ARGS)
{
return -1; /* 参数过多 */
}

cmd->argc = argc;
return 0;
}

最后对命令进行处理,比如错误的命令,正常LED控制命令的执行等等:

static int Shell_ExecuteCommand(shell_cmd_t *cmd)
{
if (cmd->argc == 0)
{
return 0;
}

/* 转换为小写进行命令匹配 */
char *cmd_name = cmd->argv[0];
for (int i = 0; cmd_name[i]; i++)
{
/* 只处理字母字符,其他字符(如\r)保持不变 */
if ((cmd_name[i] >= 'A') && (cmd_name[i] <= 'Z'))
{
cmd_name[i] = cmd_name[i] - 'A' + 'a';
}
}

/* 帮助命令 */
if (strcmp(cmd_name, "help") == 0)
{
Shell_PrintHelp();
return 0;
}

/* LED控制命令 */
if (strcmp(cmd_name, "led") == 0)
{
return LED_ProcessCommand(cmd->argc, cmd->argv);
}

/* 停止LED命令 */
if (strcmp(cmd_name, "stop") == 0)
{
/* 关闭LED */
LED_SetDutyCycle(0, 0, 0);
g_led_state.enabled = false;
g_led_state.mode = LED_MODE_INVALID;
PRINTF("LED stopped.\r\n");
return 0;
}

/* 未知命令 */
PRINTF("Unknown command: '%s'. Type 'help' for available commands.\r\n", cmd->argv[0]);
return -1;
}

LED的控制通过对相应的变量进行控制实现:

typedef struct {
led_mode_t mode; /* 当前LED模式 */
led_freq_t freq; /* 频率 */
marquee_submode_t submode; /* 跑马灯子模式 */
bool enabled; /* LED功能是否使能 */
uint32_t tick_count; /* 系统tick计数 */

/* PWM相关 */
uint8_t pwm_counter; /* PWM计数器 (0-255) */
uint8_t target_r; /* 目标R亮度 (0-255) */
uint8_t target_g; /* 目标G亮度 (0-255) */
uint8_t target_b; /* 目标B亮度 (0-255) */
uint8_t current_r; /* 当前R亮度 (用于渐变) */
uint8_t current_g; /* 当前G亮度 (用于渐变) */
uint8_t current_b; /* 当前B亮度 (用于渐变) */

/* 呼吸模式相关 */
uint8_t breath_phase; /* 呼吸相位 (0-255) */

/* 跑马灯相关 */
uint8_t marquee_index; /* 跑马灯当前LED索引 */
uint32_t marquee_last_change; /* 上次颜色切换时间 */
} led_state_t;

实际上定时器中LED的更新是一直执行的,模式不同导致的效果不同:

void LED_Update(void)
{
if (!g_led_state.enabled)
{
return;
}

/* 更新系统tick计数 */
g_led_state.tick_count = g_systick_ms;

/* 根据模式更新LED */
switch (g_led_state.mode)
{
case LED_MODE_BLINK:
LED_UpdateBlink();
break;

case LED_MODE_BREATH:
LED_UpdateBreath();
break;

case LED_MODE_MARQUEE:
LED_UpdateMarquee();
break;

default:
break;
}
}

四、效果展示

控制效果命令如下:


0

某个模式下的RGBLED的效果如下:


0

五、心得体会

本项目采用非阻塞式Shell交互设计,通过轮询UART状态寄存器实现字符接收,避免阻塞调用影响LED动画流畅运行。命令解析采用经典argc/argv风格,将输入字符串按空格分割后进行参数校验和模式分发,结构清晰且易于扩展。状态管理方面设计了统一的led_state_t结构体,集中存储当前效果模式、频率参数、子模式状态及计时信息,效果切换仅需修改mode字段即可完成平滑过渡。时间基准由CTimer1产生1ms定时中断驱动,中断服务程序中递增g_systick_ms并调用LED_Update(),确保LED更新与主程序执行解耦。整体设计遵循高内聚低耦合原则,Shell与LED控制通过函数调用接口通信,互不干扰。状态机模式便于新增效果类型,参数校验机制保证输入合法性,非阻塞架构使动画实时性与用户交互性得以兼顾。这些设计思想实际上有一部分是AI辅助的结果,确实对个人今后的程序编程有很大影响。

附件下载
frdmmcxa346_hello_world.zip
团队介绍
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号