一、任务活动介绍
Funpack 活动是由硬禾科技联合 DigiKey 发起的“玩成功就全额退”活动。本期为第五季第 1 期,指定开发平台为 NXP 新品 FRDM-MCXA346 开发板,任务内容如下:
任务 1:串口通信
- 基础题:通过 UART 串口输出字符串“Hello, DigiKey Funpack 5-1”【⭐️】
- 进阶题:基于开发板并结合 AI 辅助实现一个带缓冲区的轻量级 Shell 交互系统,可读取、解析并执行用户输入命令;支持命令提示符显示、缓冲区管理与基本命令解析,例如通过指令控制板载 LED 的颜色与亮度【⭐️⭐️⭐️】
任务 2:ADC 数据采集
- 基础题:使用高分辨率 ADC 采集电压并显示【⭐️】
- 进阶题:联合电机驱动板实现三相无刷电机驱动【⭐️⭐️⭐️】
本项目完成了任务 1 的基础与进阶内容,以及任务 2 的进阶内容。
在电机控制部分,未采用 ADC 采样方式,而是通过 I²C 读取角度传感器数据实现闭环控制。

二、项目描述
FRDM-MCXA346 开发板具备 180 MHz 主频、1 MB Flash 与 256 KB RAM,板载资源丰富,包含调试器、按键、RGB LED 及 Arduino 兼容扩展接口。
本项目围绕开发板与外设联调,依次实现以下功能:
- 串口输出与回显“Hello, DigiKey Funpack 5-1”
- 基于 Letter-Shell 实现带缓冲的命令解释器
- 通过串口命令控制 RGB LED 的开关与颜色
- 基于 SimpleFOC 完成无刷电机位置/速度/急停控制
- 将电机角度映射至 LED 颜色,实现交互联动效果
为实现上述功能,主要完成了以下工作:
- 引入 FreeRTOS 并创建多任务系统
- 移植 Letter-Shell,实现串口 Shell 与命令注册机制
- 移植 SimpleFOC,完成 PWM 与时钟配置及电机控制
- 建立 电机-传感器-LED 反馈联动链路
三、芯片选型

1)FRDM-MCXA346 开发板
- Cortex-M33 内核,最高 180 MHz
- 1 MB Flash + 256 KB RAM(含 8 KB ECC)
- 双 FlexPWM、4×16 位 ADC、SmartDMA、MAU 数学加速器
- 支持 CAN/I3C/SPI/I²C/UART/USB-Type-C
- 板载 MCU-Link 调试器(CMSIS-DAP)
- 兼容 Arduino / mikroBUS / PMOD 扩展生态
- 适用于:工业控制、电机驱动、IoT 边缘计算
本项目实际使用的资源:
调试接口、UART、I²C、PWM、板载 RGB LED
2)AS5600 磁编码角度传感器
用于实时检测无刷电机转子角度,实现位置闭环控制。
3)DRV8313 三相无刷电机驱动芯片
提供三相驱动能力,并支持外部 PWM 控制。
四、方案框图与设计思路
项目基于 NXP SDK + FreeRTOS 架构开发。
系统初始化完成后创建两个核心任务:
- Shell 任务:负责命令解析与用户交互
- FOC 任务:执行电机控制与角度采样处理
二者通过 Shell 调用相关接口,实现 Hello/LED/电机 等控制指令。

五、电路设计
- 原理图
- FRDM-MCXA346 参考原理图来源:电子森林资源库:FRDM-MCXA346 开发板
- 连线图
- I²C → AS5600 角度传感器
- PWM → DRV8313 三相驱动模块
- 板载 RGB LED 作为状态与角度显示输出

六、软件设计与关键代码
- 软件设计流程

- 工程介绍
工程以官方 freertos_hello 示例为基础:
- 启用必要外设与引脚
- 移植 Letter-Shell 与 SimpleFOC
- 注册自定义 Shell 命令

启用并配置了如下的一些引脚和外设。

- 关键代码
这里补充一些移植过程中,我觉得比较关键的代码,完整代码可以查看附件。
① Letter-Shell移植代码shell_port.c:这里使用了串口中断,并配置了循环缓冲区,用于更好地处理串口数据
Shell shell;
static char shellBuffer[512];
/* ========== UART RX Interrupt Handler ========== */
// 中断接收串口数据,放入循环缓冲区中
/**
* @brief UART RX interrupt handler
* Stores received data in ring buffer
*/
void LPUART2_IRQHandler(void)
{
uint8_t data;
/* Check if RX data available */
if (kLPUART_RxDataRegFullFlag & LPUART_GetStatusFlags(SHELL_UART))
{
data = LPUART_ReadByte(SHELL_UART);
/* Store in ring buffer if space available */
uint16_t nextHead = (rxHead + 1) % RING_BUFFER_SIZE;
if (nextHead != rxTail)
{
rxRingBuf[rxHead] = data;
rxHead = nextHead;
}
/* If buffer full, data is dropped */
}
SDK_ISR_EXIT_BARRIER;
}
/* ========== Shell Initialization ========== */
/**
* @brief Initialize shell system (UART + task + buffer)
* @return 0 on success, -1 on failure
*/
signed short shell_init(void)
{
/* Initialize UART */
shell_uart_init();
/* Configure shell structure */
shell.read = shellRead;
shell.write = shellWrite;
/* Initialize shell with buffer */
shellInit(&shell, shellBuffer, sizeof(shellBuffer));
/* Create shell task */
if (xTaskCreate(shell_task,
"shell_task",
configMINIMAL_STACK_SIZE + 512,
NULL,
tskIDLE_PRIORITY + 3,
NULL) != pdPASS)
return -1;
return 0;
}
② FOC移植foc_port.c:初始化、获取时钟、三相PWM初始化。注意原有的从systick获取的时钟无效,这里使用了freertos的时钟数据。三相PWM配置为中心对称模式,并且设置了死区时间200ns。
* ========== FOC Hardware Initialization ========== */
/**
* @brief Initialize SimpleFOC hardware and parameters
*/
void simple_foc_init(void)
{
PRINTF("\r\n=== SimpleFOC init (MCXA346) ===\r\n");
/* Initialize magnetic sensor */
MagneticSensor_Init();
delay_ms(10);
M1_Disable; // Disable driver at startup for safety
/* Initialize PWM for 3-phase output to DRV8313 */
foc_pwm_init(20000); // 20kHz recommended
/* Set FOC control parameters */
voltage_power_supply = 12.0f;
voltage_limit = 3.0f;
velocity_limit = 20.0f;
voltage_sensor_align = 3.0f;
torque_controller = Type_voltage;
controller = Type_angle;
target = 0.0f;
/* Initialize motor and FOC */
Motor_init();
Motor_initFOC();
PID_init();
PRINTF("Motor ready.\r\n");
}
/**
* @brief SimpleFOC control task
* Runs FOC loop at 1ms intervals, or LED control in angle mode
*/
static void simple_foc_task(void *arg)
{
const TickType_t period = pdMS_TO_TICKS(1); // 1ms control period
TickType_t tick = xTaskGetTickCount(); // Initialize baseline tick
for (;;)
{
if (led_angle_mode) {
// LED angle control mode: only read angle and update LEDs
float angle = getAngle();
update_leds_from_angle(angle);
} else {
// Normal FOC control
loopFOC(); // Clarke/Park/SVPWM/Current loop
move(target); // Velocity/position controller (outer loop)
}
vTaskDelayUntil(&tick, period); // Fixed period, no jitter
}
}
/**
* @brief Get current time in microseconds
* @return Current time in microseconds
*/
uint64_t micros(void)
{
uint32_t tick = xTaskGetTickCount();
uint32_t cycles = SysTick->VAL;
return tick * 1000ULL +
(SystemCoreClock / 1000U - cycles) / (SystemCoreClock / 1000000U);
}
/* ========== I2C/LPI2C Initialization ========== */
/**
* @brief Initialize I2C for FOC sensor communication
*/
void FOC_I2C_Init(void)
{
lpi2c_master_config_t config;
LPI2C_MasterGetDefaultConfig(&config);
config.baudRate_Hz = 1000000U; // Can be adjusted to 100k/400k as needed
LPI2C_MasterInit(FOC_LPI2C_BASE, &config, FOC_LPI2C_CLOCK_FREQ);
}
/******************************************************************************/
// FOC PWM初始化
#define BOARD_PWM_BASEADDR (FLEXPWM0)
#define PWM_SRC_CLK_FREQ (CLOCK_GetFreq(kCLOCK_MainClk))
#define DEMO_PWM_CLOCK_DEVIDER (kPWM_Prescale_Divide_4)
#define DEMO_PWM_FAULT_LEVEL true
/*******************************************************************************
* Code
******************************************************************************/
static void PWM_DRV_Init3PhPwm(uint32_t pwm_freq_hz)
{
uint16_t deadTimeVal;
pwm_signal_param_t pwmSignal[2];
uint32_t pwmSourceClockInHz;
uint32_t pwmFrequencyInHz = pwm_freq_hz;
pwmSourceClockInHz = PWM_SRC_CLK_FREQ;
/* Set deadtime count, we set this to about 200ns */
deadTimeVal = ((uint64_t)pwmSourceClockInHz * 200) / 1000000000;
// 省略中间代码
/*********** PWMA_SM1 - phase B configuration, setup PWM A channel only ************/
#ifdef DEMO_PWM_CLOCK_DEVIDER
PWM_SetupPwm(BOARD_PWM_BASEADDR, kPWM_Module_1, pwmSignal, 1, kPWM_SignedCenterAligned, pwmFrequencyInHz,
pwmSourceClockInHz / (1 << DEMO_PWM_CLOCK_DEVIDER));
#else
PWM_SetupPwm(BOARD_PWM_BASEADDR, kPWM_Module_1, pwmSignal, 1, kPWM_SignedCenterAligned, pwmFrequencyInHz,
pwmSourceClockInHz);
#endif
/*********** PWMA_SM2 - phase C configuration, setup PWM A channel only ************/
#ifdef DEMO_PWM_CLOCK_DEVIDER
PWM_SetupPwm(BOARD_PWM_BASEADDR, kPWM_Module_2, pwmSignal, 1, kPWM_SignedCenterAligned, pwmFrequencyInHz,
pwmSourceClockInHz / (1 << DEMO_PWM_CLOCK_DEVIDER));
#else
PWM_SetupPwm(BOARD_PWM_BASEADDR, kPWM_Module_2, pwmSignal, 1, kPWM_SignedCenterAligned, pwmFrequencyInHz,
pwmSourceClockInHz);
#endif
}
③ LED随电机角度控制foc_port.c:虽然我很想使用PWM控制LED的亮度,但是由于三个LED的引脚,无法在PWM0或1的A或B通道,部分都在X通道。其中只有A和B通道才能输出PWM,因此没办法实现亮度的控制。所以,这里只是将电机分为了12个区域,对应区域开关LED。
/**
* @brief Control LEDs based on motor angle
* Maps angle to 6 regions for RGB LED control
*/
void update_leds_from_angle(float angle_rad)
{
// Convert radians to degrees
float angle = angle_rad * 180.0f / 3.141592653589793f;
// Normalize angle to 0-360 degrees
while (angle < 0) angle += 360.0f;
while (angle >= 360.0f) angle -= 360.0f;
int r = 0, g = 0, b = 0;
// 6 regions: 3 main + 3 intermediate
if (angle < 30.0f) {
// Region 1: Red only
r = 1; g = 0; b = 0;
} else if (angle < 60.0f) {
// Region 1-2: Red + Green
r = 1; g = 1; b = 0;
} else if (angle < 90.0f) {
// Region 2: Green only
r = 0; g = 1; b = 0;
} else if (angle < 120.0f) {
// Region 2-3: Green + Blue
r = 0; g = 1; b = 1;
} else if (angle < 150.0f) {
// Region 3: Blue only
r = 0; g = 0; b = 1;
} else if (angle < 180.0f) {
// Region 3-1: Blue + Red
r = 1; g = 0; b = 1;
} else if (angle < 210.0f) {
// Region 1: Red only (continued)
r = 1; g = 0; b = 0;
} else if (angle < 240.0f) {
// Region 1-2: Red + Green (continued)
r = 1; g = 1; b = 0;
} else if (angle < 270.0f) {
// Region 2: Green only (continued)
r = 0; g = 1; b = 0;
} else if (angle < 300.0f) {
// Region 2-3: Green + Blue (continued)
r = 0; g = 1; b = 1;
} else if (angle < 330.0f) {
// Region 3: Blue only (continued)
r = 0; g = 0; b = 1;
} else {
// Region 3-1: Blue + Red (continued)
r = 1; g = 0; b = 1;
}
// Set LEDs (assuming active low)
if (r) GPIO_PortClear(BOARD_LED_RED_GPIO, 1u << BOARD_LED_RED_GPIO_PIN);
else GPIO_PortSet(BOARD_LED_RED_GPIO, 1u << BOARD_LED_RED_GPIO_PIN);
if (g) GPIO_PortClear(BOARD_LED_GREEN_GPIO, 1u << BOARD_LED_GREEN_GPIO_PIN);
else GPIO_PortSet(BOARD_LED_GREEN_GPIO, 1u << BOARD_LED_GREEN_GPIO_PIN);
if (b) GPIO_PortClear(BOARD_LED_BLUE_GPIO, 1u << BOARD_LED_BLUE_GPIO_PIN);
else GPIO_PortSet(BOARD_LED_BLUE_GPIO, 1u << BOARD_LED_BLUE_GPIO_PIN);
}
④ Shell函数、命令注册freertos_hello.c:这里简单附上几个函数和命令,例如hello、角度控制、速度控制、led设置。
/* ========== Shell hello 函数 ========== */
void hello(void)
{
PRINTF("Hello, DigiKey Funpack 5-1\r\n");
}
SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0) | SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC), hello, hello, Hello DigiKey Funpack 5 - 1);
/* ============================================================
* angle —— Query / Set target angle
* ========================================================== */
/**
* @brief Set or query motor angle target
* Usage: angle [target_degrees]
*/
void angle(int argc, void *argv[])
{
float current_angle = getAngle();
if (argc == 1)
{
PRINTF("Current angle = %.2f deg, Target = %.2f deg\r\n", current_angle, target);
return;
}
if (argc == 2)
{
const char *arg = (const char *)argv[1];
target = strtof(arg, NULL);
controller = Type_angle;
PRINTF("Set angle target = %.2f deg\r\n", target);
return;
}
PRINTF("Usage: angle [target_degrees]\r\n");
}
SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0) | SHELL_CMD_TYPE(SHELL_TYPE_CMD_MAIN),
angle, angle, angle[target]);
/* ============================================================
* vel —— Query / Set velocity target
* ========================================================== */
/**
* @brief Set or query motor velocity target
* Usage: vel [target_rad_per_sec]
*/
void vel(int argc, void *argv[])
{
if (argc == 1)
{
PRINTF("Velocity target = %.2f rad/s\r\n", target);
return;
}
target = strtof((char *)argv[1], NULL);
controller = Type_velocity;
PRINTF("Set velocity target = %.2f rad/s\r\n", target);
}
SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0) | SHELL_CMD_TYPE(SHELL_TYPE_CMD_MAIN),
vel, vel, vel[rad / s]);
七、实物演示
- 使用USB连接上开发板,通过MobaXterm创建一个串口的Session进行连接。
- 上电之后,会输出FOC初始化的调试信息,期间电机会旋转小的角度进行校准。校准完成之后,会显示输入密码(123456),成功后即可进入shell交互。

- 输入help查看所有的指令,输入hello可以返回Hello, DigiKey Funpack 5-1,输入angle获取角度。

- 输入led查看相关指令,并通过例如led on, led off, led rgb 1 1 0等,控制led的开关

- 输入angle查看角度,带上数字为目标角度,可以看到电机旋转;或者输入vel控制旋转速度;以及输入stop停止电机。


- 输入ledmode on,即可开启电机控制led颜色函数。具体效果可以从视频中查看。
八、难点与解决
本次项目遇到了很多问题。虽然基础题目很简单,但是看到开发板的性能如何强悍,决定完成进阶功能,为此遇到了如下问题并解决:
- 难点:Letter-Shell中不能够注册自己的函数,并且IDE中无法修改链接文件。
解决:需要在项目属性中,将该项目的链接文件管理功能关闭,即可修改链接文件。随后,在链接文件的text部分,补充上代码。
- 难点:在无刷电机控制中,PWM始终无法输出
解决:官方案例中提供的PWM工程,就是完成三相输出的PWM,为此,修改其中的频率、占空比函数接口完成PWM输出。
- 难点:移植的SimpleFOC无法正常工作,电机抖动厉害。
解决:移植过程中,其中的获取系统时钟的函数SysTick(),因为开发板也包括该函数所以没报错,但实际上无输出。为此,修改时钟为freertos的即可。
- 难点:Letter-Shell和FOC的任务,由于实时性要求高,两个任务难以在Freertos中同时工作,即使修改任务优先级也不可以。
解决:在Freertos的配置中,将其频率从200Hz修改为5000Hz,即可平衡两个任务,其中FOC任务以1kHz运行,Letter-Shell任务以低优先级循环运行即可。
- 难点:一直想要通过PWM控制板载LED的亮度,但是一直无法实现,最多只能控制BLUE LED的亮度。
解决:最后发现是因为AB通道才能输出PWM,X通道只能用于捕获,因此无法控制亮度。
九、总结
- 通过本次项目实践,系统掌握了 FRDM-MCXA346 的外设配置流程以及 FreeRTOS 多任务开发方法;
- 完成了 Letter-Shell 与 SimpleFOC 的移植与调试,并对移植过程中可能出现的时序、时钟与外设冲突问题形成了完整的认识与经验积累。
- 最后感谢主办方给予的机会!
