👉 【Funpack3-3】
👉 Github: EmbeddedCamerata/FRDM-MCXN947_lwip_peripherals_control
项目介绍
基于 NXP FRDM-MCXN947 开发板,通过板卡上的 RJ45 以太网口与 PC 连接,使用 LwIP 协议与主机进行数据传输。开发板集成有温度传感器、按键、触摸按键等外设,板卡可将温度,触摸与按键信息发送至主机;主机可通过 LwIP 控制板卡 RGB LED。
👉 MCUXpresso
👉 MCUXpresso VSCode 扩展
硬件介绍
FRDM-MCXN947 是一款紧凑且可扩展的开发板,可让您快速基于 MCX N94x MCU 开展原型设计。板卡上集成行业标准的接口,可轻松使用 MCU 的I/O、集成的开放标准串行接口、外部闪存和板载 MCU-Link 调试器,支持各种附加版的扩展,为用户提供简单高效的评估和开发体验。
该板由一个 MCXN947 器件和一个64 mbit外部串行闪存组成。该板还具有 P3T1755DP I3C 温度传感器、TJA1057GTK/3Z CAN PHY、以太网 PHY、SDHC电路、RGB LED、触摸板、高速 USB 电路、按钮等 MCU-Link 调试器电路。该板提供 Arduino 扩展、PMOD 拓展,MicroBus 总线拓展。该板还支持摄像头模块和 NXP 低成本 LCD 模块 PAR-LCD-S035。板载 MCU-Link 调试器基于 LPC55S69 MCU。
项目设计
开发环境
本项目使用 MCUXpresso VSCode 扩展开发。项目所用到的工具链、库如下:
- 编译器:arm-none-eabi-gcc v13.3.1
- 调试器:arm-none-eabi-gdb v14.2
- MCUXpresso SDK:SDK_2.x_FRDM-MCXN947 v2.16
- Linkserver:v1.6.121,启动和管理恩智浦硬件调试器的GDB服务器,并提供命令行目标闪存编程功能
总体流程图
首先初始化时钟与外设,而后创建三个外设相关的读取任务,用于周期性读取并记录它们的状态。之后进行 LwIP 初始化,进行以太网初始化、IPv4 地址设置,再创建主任务,socket 创建、绑定、监听,并在死循环中等待 PC 连接。当 PC 连接后,创建数据发送任务,每秒发送外设的状态信息;且接收 PC 发送的命令,进行字符串匹配,根据匹配结果控制对应 REB LED 。当 PC 断连后,删除数据发送任务,并等待下次连接。
功能实现
由于使用的是 MCUXpresso VSCode 扩展,因此本项目基于多个示例工程进行开发。本项目将参考官方提供的多个相关模块的示例代码进行移植与修改。当安装好对应开发板的 SDK 后,可通过扩展内的“Import Example from Repository”选项导入示例程序。
LED控制、按键读取
参考示例工程 frdmmcxn947_gpio_input_interrupt 中的对 GPIO 的配置、读写功能。
/* Define the init structure for the input switch pin */
gpio_pin_config_t sw_config = {
kGPIO_DigitalInput,
0,
};
GPIO_PinInit(BOARD_SW3_GPIO, BOARD_SW3_GPIO_PIN, &sw_config);
/* LED init */
LED_RED_INIT(LOGIC_LED_OFF);
LED_GREEN_INIT(LOGIC_LED_OFF);
LED_BLUE_INIT(LOGIC_LED_OFF);
针对 RGB LED 的操作可通过宏 LED_xxx_INIT
、LED_xxx_ON
、LED_xxx_OFF
、LED_xxx_TOGGLE
完成,xxx
为对应颜色。针对 SW3,将其配置成输入,通过 GPIO_PinRead()
可读取其值,0表示按下。将按键读取功能单独写成函数,后续将作为一个单独的任务在 FreeRTOS 中创建。
/* Whether the SW button is pressed */
volatile uint32_t g_ButtonStatus = 0;
static void sw_scan_task(void *arg)
{
while (1)
{
if ((g_ButtonStatus = GPIO_PinRead(BOARD_SW3_GPIO, BOARD_SW3_GPIO_PIN)) == 0)
{
PRINTF("%s is pressed\n", BOARD_SW3_NAME);
}
vTaskDelay(200 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}
温度传感器测量与读取
参考示例工程 frdmmcxn947_lpadc_temperature_measurement 中初始化温度传感器、读取温度的代码,该工程通过设置 ADC 软件中断进行温度测量, g_LpadcConversionCompletedFlag
用于表示温度测量是否完成,测得的温度保存至 g_CurrentTemperature
中。读取温度数值的代码单独写成函数,后续将作为一个单独的任务在 FreeRTOS 中创建。
volatile bool g_LpadcConversionCompletedFlag = false;
float g_CurrentTemperature = 0.0f;
static void temp_measurement_task(void *arg)
{
while (1)
{
g_LpadcConversionCompletedFlag = false;
LPADC_DoSoftwareTrigger(LPADC_BASE, 1U); /* 1U is trigger0 mask. */
while (false == g_LpadcConversionCompletedFlag)
;
vTaskDelay(500 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}
触摸按键读取
参考示例工程 frdmmcxn947_tsi_v6_self_cap 中提供的软件中断触发扫描(示例还提供了硬件触发扫描、软件轮询触发扫描的方法)的方法实现对触摸按键的读取检测。除了打开必要的时钟外,三种扫描方法对TSI初始化的配置是相同的:
tsi_calibration_data_t buffer;
void TSI0_IRQHandler(void)
{
...
}
int main(void)
{
/* Configure LPTMR */
LPTMR_GetDefaultConfig(&lptmrConfig);
/* TSI default hardware configuration for self-cap mode */
TSI_GetSelfCapModeDefaultConfig(&tsiConfig_selfCap);
/* Initialize the LPTMR */
LPTMR_Init(LPTMR0, &lptmrConfig);
/* Initialize the TSI */
TSI_InitSelfCapMode(APP_TSI, &tsiConfig_selfCap);
/* Enable noise cancellation function */
TSI_EnableNoiseCancellation(APP_TSI, true);
/* Set timer period */
LPTMR_SetTimerPeriod(LPTMR0, USEC_TO_COUNT(LPTMR_USEC_COUNT, LPTMR_SOURCE_CLOCK));
NVIC_EnableIRQ(TSI0_IRQn);
TSI_EnableModule(APP_TSI, true); /* Enable module */
/********* CALIBRATION PROCESS ************/
memset((void *)&buffer, 0, sizeof(buffer));
TSI_SelfCapCalibrate(APP_TSI, &buffer);
}
当触摸按键未被按下时,s_tsiInProgress
为 true ,当按下超过一定时长后,触发软件中断,在回调函数 TSI0_IRQHandler
中,s_tsiInProgress
置 false 。按下过程中,将触发多次软件中断。当松手后,不会触发软件中断,s_tsiInProgress
将置 true,将检测触摸按键的代码单独写成函数,后续将作为一个单独的任务在 FreeRTOS 中创建。
static void tsi_scan_task(void *arg)
{
while (1)
{
while (s_tsiInProgress)
{
TSI_StartSoftwareTrigger(APP_TSI);
}
s_tsiInProgress = true;
vTaskDelay(100 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}
LwIP通信
本项目主体代码参考示例工程 frdmmcxn947_lwip_ping_freertos,该示例提供了完整的IPv4地址初始化、网口初始化、FreeRTOS配置等,后续只需要将示例中所使用的 ping 功能替换为我们的业务代码即可。
LwIP参考:[野火]LwIP应用开发实战指南—基于STM32
首先配置开发板的IPv4地址 configIP_ADDR0/1/2/3
与所连接的网关的地址 configGW_ADDR0/1/2/3
。
#define TCP_PORT 5001
static void lwip_tcp_task(void *arg)
{
LWIP_UNUSED_ARG(arg);
int err, sock;
struct timeval timeout = {.tv_sec = 1, .tv_usec = 0};
struct sockaddr_in server_addr, client_addr;
int connected = -1;
socklen_t sin_size = sizeof(struct sockaddr_in);
char *recv_cmd;
ssize_t recv_cmd_len;
char unknown_cmd_str[] = "Unknown command\n";
recv_cmd = (char *)pvPortMalloc(LWIP_RECV_DATA_SIZE);
LWIP_ERROR("no memory.", recv_cmd != NULL, goto __exit;);
sock = lwip_socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
LWIP_ERROR("socket build failed.", sock >= 0, return;);
err = lwip_setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
LWIP_ERROR("setting receive timeout failed.", err == 0, goto __exit;);
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = lwip_htons(TCP_PORT);
memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));
/* Socket bind */
err = lwip_bind(sock, (struct sockaddr *)&server_addr, sizeof(server_addr));
LWIP_ERROR("bind failed.", err == 0, goto __exit;);
/* Listen */
err = lwip_listen(sock, 5);
LWIP_ERROR("listen failed.", err == 0, goto __exit;);
while (1)
{
...
}
vTaskDelete(NULL);
__exit:
if (recv_cmd)
vPortFree(recv_cmd);
if (sock >= 0)
lwip_close(sock);
vTaskDelete(NULL);
}
通过 lwip_socket
、lwip_setsockopt
创建一个 socket 接口并配置,lwip_bind
将申请成功的 socket 与网卡信息进行绑定,绑定到5001端口。recv_cmd
后续将接收从 PC 发送来的数据。后续,在 while(1)
循环中,通过 lwip_accept
判断 PC 是否发出连接请求,若请求则返回描述符。若连接,则创建一个发送任务,用于定时向 PC 发送传感器、按键信息。
while (1)
{
connected = lwip_accept(sock, (struct sockaddr *)&client_addr, &sin_size);
if (connected > 0)
{
PRINTF("new client connected from (%s, %d)\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
if (send_data_task_handle == NULL)
{
if (xTaskCreate(send_data_task, "send_data_task", 512,
(void *)&connected, 4, &send_data_task_handle) != pdPASS)
{
PRINTF("lwip_tcp_task(): Task creation failed!\n");
while (1)
;
}
}
while (1)
{
...
}
else
{
if (send_data_task_handle != NULL)
{
vTaskDelete(send_data_task_handle);
send_data_task_handle = NULL;
}
}
vTaskDelay(500 / portTICK_PERIOD_MS);
}
同时,当 PC 连接后,阻塞接收从 PC 传输的数据,并进行字符串匹配,根据匹配的结果“blue”/“green”/"red"控制对应的 LED 状态翻转;匹配到“close”则关闭连接;未匹配则发送字符串至 PC 进行提示。
#define LWIP_RECV_DATA_SIZE 128
while (1)
{
memset(recv_cmd, 0, LWIP_RECV_DATA_SIZE);
recv_cmd_len = lwip_recv(connected, recv_cmd, LWIP_RECV_DATA_SIZE, 0);
if (recv_cmd_len <= 0)
break;
/* Command match */
if (strcmp(recv_cmd, "close") == 0)
{
err = lwip_close(connected);
LWIP_ERROR("close failed", err == 0, goto __exit;);
connected = -1;
break;
}
else if (strcmp(recv_cmd, "blue") == 0)
{
LED_BLUE_TOGGLE();
}
else if (strcmp(recv_cmd, "green") == 0)
{
LED_GREEN_TOGGLE();
}
else if (strcmp(recv_cmd, "red") == 0)
{
LED_RED_TOGGLE();
}
else // Send unknown command message
{
lwip_write(connected, (void *)unknown_cmd_str, strlen(unknown_cmd_str));
}
}
该任务实现每秒通过 lwip_write
向 PC 发送温度传感器读数、按键、触摸按键信息。它在 LwIP 连接到 PC 后创建(若不存在则创建),若连接关闭则任务将被删除。
#define LWIP_SEND_DATA_SIZE 128
static void send_data_task(void *arg)
{
int connected = *(int *)arg;
int temp_int, temp_deci;
char *send_data;
send_data = (char *)pvPortMalloc(LWIP_SEND_DATA_SIZE);
LWIP_ERROR("no memory", send_data != NULL, goto __exit;);
LWIP_UNUSED_ARG(arg);
while (1)
{
temp_int = (int)g_CurrentTemperature;
temp_deci = (int)(100 * (g_CurrentTemperature - temp_int));
sprintf(send_data, "temp: %02d.%02d, touch: %d, sw3: %lu\n",
temp_int, temp_deci, 1 - s_tsiInProgress, 1 - g_ButtonStatus);
lwip_write(connected, (void *)send_data, strlen(send_data));
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
__exit:
if (send_data)
vPortFree(send_data);
vTaskDelete(NULL);
}
编译配置
需要根据前述所参考的工程,将它们的 pin_mux.c
、board.h
综合起来,这里面主要是针对 GPIO 或板卡硬件相关参数的定义。同时,还需要为 armgcc/config.cmake
添加额外用到的模块,具体可通过前述示例工程内的 armgcc/config.cmake
进行比较可得。将它们汇总至本工程下 armgcc/config.cmake
即可。
set(CONFIG_COMPILER gcc)
set(CONFIG_TOOLCHAIN armgcc)
set(CONFIG_USE_COMPONENT_CONFIGURATION false)
set(CONFIG_USE_driver_phy-device-lan8741 true)
set(CONFIG_USE_driver_vref_1 true)
set(CONFIG_USE_driver_crc true)
set(CONFIG_USE_utility_debug_console_lite true)
set(CONFIG_USE_utility_assert_lite true)
set(CONFIG_USE_driver_lpadc true)
set(CONFIG_USE_driver_flashiap true)
set(CONFIG_USE_component_silicon_id true)
set(CONFIG_USE_middleware_lwip_mcx_ethernetif true)
set(CONFIG_USE_middleware_lwip true)
set(CONFIG_USE_driver_mcx_enet true)
set(CONFIG_USE_middleware_freertos-kernel_cm33_non_trustzone true)
set(CONFIG_USE_middleware_lwip_contrib_ping true)
set(CONFIG_USE_driver_clock true)
set(CONFIG_USE_middleware_freertos-kernel_heap_3 true)
set(CONFIG_USE_driver_common true)
set(CONFIG_USE_driver_inputmux true)
set(CONFIG_USE_driver_power true)
set(CONFIG_USE_driver_reset true)
set(CONFIG_USE_device_MCXN947_CMSIS true)
set(CONFIG_USE_device_MCXN947_startup true)
set(CONFIG_USE_driver_lptmr true)
set(CONFIG_USE_driver_tsi_v6 true)
set(CONFIG_USE_driver_lpuart true)
set(CONFIG_USE_driver_gpio true)
set(CONFIG_USE_driver_port true)
set(CONFIG_USE_component_lpuart_adapter true)
set(CONFIG_USE_component_lists true)
set(CONFIG_USE_driver_reset true)
set(CONFIG_USE_utilities_misc_utilities true)
set(CONFIG_USE_utility_str true)
set(CONFIG_USE_driver_phy-common true)
set(CONFIG_USE_middleware_lwip_template true)
set(CONFIG_USE_component_gpio_adapter true)
set(CONFIG_USE_middleware_freertos-kernel true)
set(CONFIG_USE_middleware_freertos-kernel_template true)
set(CONFIG_USE_middleware_freertos-kernel_extension true)
set(CONFIG_USE_driver_mcx_spc true)
set(CONFIG_USE_driver_inputmux_connections true)
set(CONFIG_USE_CMSIS_Include_core_cm true)
set(CONFIG_USE_device_MCXN947_system true)
set(CONFIG_USE_driver_lpflexcomm true)
set(CONFIG_CORE cm33)
set(CONFIG_DEVICE MCXN947)
set(CONFIG_BOARD frdmmcxn947)
set(CONFIG_KIT frdmmcxn947)
set(CONFIG_DEVICE_ID MCXN947)
set(CONFIG_FPU SP_FPU)
set(CONFIG_DSP DSP)
set(CONFIG_CORE_ID cm33_core0)
由于直接使用的 MCUX VSCode 扩展,因此未安装 newlib 库,头一次编译将报错。这时,将 armgcc/CMakePresets.json
中有关 NEWLIB 的配置删除即可。
同时,还有可能遇到成功编译、下载,但是程序卡死的情况。修改 armgcc/flags.cmake
中 linker 链接标志,位于 CMAKE_EXE_LINKER_FLAGS_DEBUG
与 CMAKE_EXE_LINKER_FLAGS_RELEASE
,修改 --defsym=__stack_size__=2000
与 --defsym=__heap_size__=38400
后再编译、运行,应该可以解决问题。
功能展示
首先通过网线将板卡与 PC 连接,在 PC 上连接该有线网。此时,串口将打印 IP 等信息。
通过 TCP 连接板卡,连接后,板卡每秒钟将通过 LwIP 发送温度、按键等信息。如下图所示,按下触摸按键,“touch btn”状态为1。
PC 也可通过 LwIP 发送数据至板卡以控制 LED。如下图所示,输入“red”,板卡红灯亮。
👉 详细展示参见:B站:基于FRDM-MCXN947的LwIP通信与外设控制
项目总结
本次项目基于 FRDM-MCXN947 与 LwIP 实现了与 PC 的通信与外设控制,了解了 LwIP 的基本使用方法、触摸按键等外设的读取。MCUX 扩展功能集成度高,项目的编译、下载均可通过扩展完成,调试也可以使用 gdb 在 VSCode 中实现。