项目描述
Funpack 第三季第四期活动,以 NXP 的 FRDM-MCXN947 板卡为主控,可选三个任务。我选择了任务2,具体要求如下:
- 使用板卡上的以太网接口连接到电脑上并通过以太网和电脑通信,实现数据传输。要求电脑可以获取到板卡上的温度,触摸和按键信息,并可以通过电脑控制板卡上的RGB LED灯(有搭配扩展模块,传感器类的在电脑上显示数据,执行器类的能用电脑控制其工作)
- 推荐搭配传感器/执行器模块:3595 、BCRRQI_AAA、VQ4TL2BQ380001
我搭配了官方推荐的三个扩展模块,分别是:
- 3595,APDS9960 环境光、接近传感器,I2C通讯接口;
- BCRRQI_AAA,L9110S 电机驱动模块;
- VQ4TLBQ380001,3.0V 圆柱直流电机;
此项目要实现的功能如下:
- 以 FRDM-MCXN947 为主控,通过以太网接口连接到网络,可与电脑通信;
- 主控读取板卡上的三个信息:温度信息,Touch Pad 触摸状态,两个按键 (SW2,SW3)的状态;通过网络上传给电脑,电脑显示对应模块的状态;
- 主控连接外接模块,读取 APDS9960 模块的环境光信息、接近信息,通过网络上传给电脑,电脑显示模块的几个数值;
- 主控连接电机驱动模块 L9110S(它的输出端接上圆柱直流电机);用户通过GUI交互,通过网络下发命令给板卡,板卡可以控制电机正转、反转和停止。
系统框图
项目系统框图如下。
左侧板卡是主控,采集板卡上的温度信息、触摸板状态信息(是否触摸)、按键状态信息(按下、释放的状态)、通过I2C接口读取 APDS9960 模块的传感器数值(环境光、接近检测数值),通过以太网连接到交换机或路由器,把信息发送给电脑。
电脑作为服务器,通过网络接收板卡上传的信息。用户可与GUI交互,点击对应的控件通过网络发送命令给板卡,使板卡控制 RGB LED 亮灭,控制直流电机正转、反转和停止。
板卡上开发的是下位机代码,以 MUCXpresso for VSCdoe 作为开发环境;电脑段开发的是上位机代码,以 Qt v6.5.2 作为开发环境。
主控介绍
NXP FRDM-MCNX947 开发板是一个基于 MCXN947 设备的低成本设计评估板。MCXN947 设备在一个封装里集成了2个 ARM Cortex-M33 微控制器和一个神经处理单元(NPU)。此板卡有一个 64Mbit 的外部串行 Flash,一个支持 I3C 接口的 P3T1755DP 温度传感器,一个 TJA1057GTK/3A CAN PHY,一个 Ethernet PHY, SDHC 电路(卡座未焊接),一个 RGB LED,一个 Touch Pad,一个高速 USB 电路,多个按键,还有板载的 MCU-Link 调试器。此开发板兼容 Arduino 模块, Pmod 开发板和 mikroBUS。该开发板还支持一个摄像头模块和 NXP 低成本的 LCD 模块 PAR-LCD-S035。
温度传感器 P3T1755DP
此板卡包含一个 P3T1755DP 数字温度传感器,以演示 MCXN947 的 I3C 能力。该传感器设备允许32个I3C临时ID,支持板卡的全工作电压,可编程的超温报警,12比特分辨率,从-20°C到+85°C的精度为 ±0.5°(最大)。
温度传感器的 7bit I2C地址为 0b1001000 即 0x48。原理图如下:
从原理图可知 I3C 通信有三个管脚需要配置:
- I3C1_PUR 信号连接到
P1_11管脚; - I3C1_SCL 信号连接到
P1_17管脚; - I3C1_SDA 信号连接到
P1_16管脚;
触摸板 TSI
一个触摸滑块用于触摸传感检测:通过 P1_3 引脚连接到 TSI 输入通道。
它的原理图如下,只需要把 P1_3 配置为 TSI 输入通道即可。
按键 SW2/3
板卡上有三个按键:
- SW1 复位按键,按下使得MCU复位、外设复位,重新执行复位代码。
- SW2 是一个通用输入,并且连接到唤醒管脚。按下时在
P0_23/WAKEUP_B管脚产生低电平,否则保持高电平; - SW3 是一个通用输入,并且可以作为 ISP 模式切换管脚。按下时在
P0_6/ISPMODE_N-DEBUG管脚产生低电平,否则保持高电平。
这里把 SW2 和 SW3 作为普通按键,按键按下和释放的状态上传给电脑。而 SW1 无法通过代码键控,忽略。
APDS9960 模块
此模块是一款8针封装的数字 RGB、环境光、接近和手势传感器设备。该设备具有 I2C 兼容的接口,可通过红外 LED 提供红、绿、蓝、清晰度、接近和手势感应。RGB 和环境光感应功能在各种照明条件下可通过衰减材料(包括变暗的玻璃)检测光强度。接近和手势功能是工厂调整和校准到 100mm 接近检测距离,无需客户校准。收拾检测利用四个定向光电二极管,集成了可见阻塞滤波器,以准确地感知简单的上下左右收拾或者更复杂的手势。模块内增加的微光学透镜提供了搞笑的红外能量传输和接受。内部状态机允许设备在 RGBC,接近和手势测量之间进入低功耗状态,提供非常低的功耗。可用于显示屏背光控制、相关色温感应、手机触摸屏禁用、数码相机触摸屏禁用、机械开关更换、手势检测等。
我的项目中只需要到 I2C 接口与此模块通信,而且是 GPIO 模拟的 I2C :
P3_16配置为I2C_SCL信号P3_20配置为I2C_SDA信号
以太网
在此板卡上,以太网控制器通过以太网 PHY 收发器连接到 RJ45 连接器。发送、接收和其他以太网信号在 P1 端口引脚上。 FRDM-MCXN947 只支持 RMII 配置。因此以太网 PHY LAN8741A-EN 的 TXD3 和 TXD2 引脚已分别通过电阻 R68 和 R67 接地。
原理图如下
RGB LED
用户应用LED。每个LED可以由用户程序控制。
它的原来原理图如下:
电机 L9110S 驱动模块
此电机驱动模块是双L9110S芯片的电机驱动模块。它支持的供电电压范围是 2.5-12V,供电电压越大则直流电机转速越快,前提不超过额定电压。
可同时驱动2个直流电机,或者1个4线2相式步进电机。
模块接口说明
6P 黑色排针说明:
- VCC外接2.5V~12V电压;
- GND 外接GND;
- IA1 外接单片机IO口;
- IB1 外接单片机IO口;
- IA2 外接单片机IO口;
- IB2 外接单片机IO口;
4P绿端子说明:
- OA1 和 OB1 接直流电机2个引脚,无方向;
- OA2 和 OB2 接直流电机2个引脚,无方向;
我的项目只用到一个电机,因此管脚配置如下:
P1_12连接到 L9110S 1A 管脚;P0_25连接到 L9110S 1B 管脚;
软件流程图及各功能对应的主要代码片段及说明
1. 下位机软件流程图
在讲解流程图之前,先申明本项目基于 MCUXpresso for VSCode 开发环境。先得安装 MCUXpresso IDE并下载一个 MCUXpress SDK 包,并且配置 MCUXpress for VSCode 才能编译下位机代码。
流程图说明
- 主程序开始进行硬件初始化,主要是管脚和时钟初始化。管脚通过 MCUXpresso Config Tools v15.1 进行配置,保存的配置文件为
frdmmcxn947_funpack3_4.mex; - 创建启动任务:启动任务中创建子任务,分别启动按键扫描任务、读取温度的任务、读取APDS9960传感器的任务、读取 Touch Pad 状态的任务,最后启动网络任务,初始化 LwIP 协议栈,并通过DHCP客户端获取IP地址。成功获取IP地址之后才启动 UDP 通信任务,连接到电脑服务器,上传各个传感器数值,接收控制信号并执行(如操作 RGB LED 和控制电机);
- 最后启动 FreeRTOS 调度器;
主函数
BOARD_InitBootClocks();
BOARD_InitBootPeripherals();
BOARD_InitBootPins();
BOARD_InitSWD_DEBUGPins();
bsp_enet_init();
EnableIRQ(BOARD_APDS9960_GPIO_IRQ);
// 调试串口打印日志
BOARD_InitDebugConsole();
PRINTF("\r\n");
PRINTF("####### ##### # \r\n");
PRINTF("# # # # # ##### ## #### # # # # # # \r\n");
PRINTF("# # # ## # # # # # # # # # # # # \r\n");
PRINTF("##### # # # # # # # # # # #### ##### # # \r\n");
PRINTF("# # # # # # ##### ###### # # # # ####### \r\n");
PRINTF("# # # # ## # # # # # # # # # # \r\n");
PRINTF("# #### # # # # # #### # # ##### # \r\n");
PRINTF(" ####### \r\n");
PRINTF("\r\n\t\t Build: %s %s\r\n\r\n", __DATE__, __TIME__);
// perf_counter 初始化, true 表示 SysTick 已经由用户初始化了,perf_counter 无需再次初始化
init_cycle_counter(true);
if (xTaskCreate(zygote_task, "zygote_task", ZYGOTE_TASK_STACK_SIZE, NULL, ZYGOTE_TASK_PRIORITY, NULL) !=
pdPASS)
{
PRINTF("Task creation failed!.\r\n");
while (1)
;
}
vTaskStartScheduler();
for (;;)
;
启动任务
此任务创建多个子任务,例如按键扫描任务、读取温度传感器的任务、读取APDS9960传感器的任务,读取TSI触摸状态的任务,以及创建网络通信任务。
static void zygote_task(void *pvParameters)
{
uint32_t zygote_loop_cnt = 0;
// 按键初始化,在此创建了扫描按键状态的软件定时器
btn_init();
temp_sensor_task_create();
// 不使用 CLI 可以注释下面的代码
vUARTCommandConsoleStart();
extern void vRegisterSampleCLICommands(void);
vRegisterSampleCLICommands();
vRegisterBspCliCommands();
apds9960_task_start();
tsi_trigger_task_create();
network_task_create();
for (;;) {
zygote_loop_cnt++;
// PRINTF("zygote loop cnt: %u \r\n", zygote_loop_cnt);
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
按键扫描任务
这里移植了一个 MultiButton 的库,参见 GitHub - 0x1abin/MultiButton: Button driver for embedded system
MultiButton 是一个小巧简单易用的事件驱动型按键驱动模块,可无限量扩展按键,按键事件的回调异步处理方式可以简化你的程序结构,去除冗余的按键处理硬编码,让你的按键业务逻辑更清晰。
注册两个按键对象 SW2 和 SW3,分别设置各种状态的回调函数,然后启动定时器开始扫描案件状态。在定时器回调函数中更新每个按键的状态。
void btn_init(void)
{
// 按键管脚初始化已经在主函数中完成
// 初始化 Button 对象
button_init(&btn_sw2, read_button_gpio, 0, BTN_ID_SW2);
button_init(&btn_sw3, read_button_gpio, 0, BTN_ID_SW3);
// 绑定按键对应的回调函数
button_attach(&btn_sw2, PRESS_DOWN, btn_sw2_press_down_cb);
button_attach(&btn_sw2, PRESS_UP, btn_sw2_press_up_cb);
button_attach(&btn_sw2, PRESS_REPEAT, btn_sw2_press_repeat_cb);
button_attach(&btn_sw2, SINGLE_CLICK, btn_sw2_single_click_cb);
button_attach(&btn_sw2, DOUBLE_CLICK, btn_sw2_double_click_cb);
button_attach(&btn_sw2, LONG_PRESS_START, btn_sw2_long_press_start_cb);
button_attach(&btn_sw2, LONG_PRESS_HOLD, btn_sw2_long_press_hold_cb);
button_start(&btn_sw2);
button_attach(&btn_sw3, PRESS_DOWN, btn_sw3_press_down_cb);
button_attach(&btn_sw3, PRESS_UP, btn_sw3_press_up_cb);
button_attach(&btn_sw3, PRESS_REPEAT, btn_sw3_press_repeat_cb);
button_attach(&btn_sw3, SINGLE_CLICK, btn_sw3_single_click_cb);
button_attach(&btn_sw3, DOUBLE_CLICK, btn_sw3_double_click_cb);
button_attach(&btn_sw3, LONG_PRESS_START, btn_sw3_long_press_start_cb);
button_attach(&btn_sw3, LONG_PRESS_HOLD, btn_sw3_long_press_hold_cb);
button_start(&btn_sw3);
// 创建软件定时器,每隔5ms调用一次 button_ticks
swTimerHdl = xTimerCreate("sw_tmr_btn", SW_TIMER_PERIOD_MS, pdTRUE, 0, SwTimerCallback);
if (NULL == swTimerHdl) {
PRINTF("Failed to create swTimerHdl!!!\r\n");
while (1);
}
xTimerStart(swTimerHdl, 0);
}
温度传感器任务
函数 temp_sensor_task_create() 只有两行代码,调用 temp_sensor_init() 通过 I3C 接口初始化温度传感器 P3T1755,然后创建任务 temp_sensor_task_entry(),在这个任务重每隔1秒钟读取一次温度数值。
通过I3C初始化温度传感器 P3T1755
void temp_sensor_init(void)
{
status_t result = kStatus_Success;
i3c_master_config_t masterConfig = { 0 };
p3t1755_config_t p3t1755Config = { 0 };
/* I3C 时钟初始化 */
/* Attach PLL0 clock to I3C1, 150MHz/6 = 25MHz */
CLOCK_SetClkDiv(kCLOCK_DivI3c1FClk, 6);
CLOCK_AttachClk(kPLL0_to_I3C1FCLK);
/* I3C 管脚初始化:已经在主函数中初始了,这里为空 */
/* I3C master 配置 */
I3C_MasterGetDefaultConfig(&masterConfig);
masterConfig.baudRate_Hz.i2cBaud = EXAMPLE_I2C_BAUDRATE;
masterConfig.baudRate_Hz.i3cPushPullBaud = EXAMPLE_I3C_PP_BAUDRATE;
masterConfig.baudRate_Hz.i3cOpenDrainBaud = EXAMPLE_I3C_OD_BAUDRATE;
masterConfig.enableOpenDrainStop = false;
masterConfig.disableTimeout = true;
I3C_MasterInit(EXAMPLE_MASTER, &masterConfig, I3C_MASTER_CLOCK_FREQUENCY);
/* Create I3C handle */
I3C_MasterTransferCreateHandle(EXAMPLE_MASTER, &g_i3c_m_handle, &masterCallback, NULL);
/* P3T1755 初始化 */
result = p3t1755_set_dynamic_address();
if (kStatus_Success != result) {
PRINTF("\r\nP3T1755 set dynamic address failed.\r\n");
}
p3t1755Config.writeTransfer = I3C_WriteSensor;
p3t1755Config.readTransfer = I3C_ReadSensor;
p3t1755Config.sensorAddress = SENSOR_ADDR;
P3T1755_Init(&p3t1755Handle, &p3t1755Config);
}
读取温度数值的任务
void temp_sensor_task_entry(void *arg)
{
status_t result = kStatus_Success;
while (1) {
result = P3T1755_ReadTemperature(&p3t1755Handle, &m_temperature);
if (kStatus_Success != result) {
PRINTF("\r\nP3T1755 read temperature failed!!!\r\n");
} else {
// TODO: 尚未支持浮点数打印,需要链接库支持
// PRINTF("\r\nTemperature: %d.%d \r\n", ((int32_t)m_temperature) % 100, ((int32_t)(m_temperature * 100)) % 100);
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
APDS9960 传感器任务
函数 apds9960_task_start() 仅有两行代码,调用 apds9960_dev_init() 通过软件模拟的I2C接口初始化 APDS9960 设备,然后创建任务 apds9960_task_entry(),在此任务中每隔1秒钟读取一次环境光数值和接近传感器数值。
APDS9960 初始化
static uint8_t apds9960_dev_init(void)
{
uint8_t res;
uint8_t reg;
uint32_t i;
apds9960_info_t info;
/* link interface function */
DRIVER_APDS9960_LINK_INIT(&gs_handle, apds9960_handle_t);
DRIVER_APDS9960_LINK_IIC_INIT(&gs_handle, apds9960_interface_iic_init);
DRIVER_APDS9960_LINK_IIC_DEINIT(&gs_handle, apds9960_interface_iic_deinit);
DRIVER_APDS9960_LINK_IIC_READ(&gs_handle, apds9960_interface_iic_read);
DRIVER_APDS9960_LINK_IIC_WRITE(&gs_handle, apds9960_interface_iic_write);
DRIVER_APDS9960_LINK_DELAY_MS(&gs_handle, apds9960_interface_delay_ms);
DRIVER_APDS9960_LINK_DEBUG_PRINT(&gs_handle, apds9960_interface_debug_print);
DRIVER_APDS9960_LINK_RECEIVE_CALLBACK(&gs_handle, apds9960_interface_receive_callback);
/* get information */
res = apds9960_info(&info);
if (res != 0)
{
apds9960_interface_debug_print("apds9960: get info failed.\n");
return 1;
}
else
{
/* print chip info */
apds9960_interface_debug_print("apds9960: chip is %s.\n", info.chip_name);
apds9960_interface_debug_print("apds9960: manufacturer is %s.\n", info.manufacturer_name);
apds9960_interface_debug_print("apds9960: interface is %s.\n", info.interface);
apds9960_interface_debug_print("apds9960: driver version is %d.%d.\n", info.driver_version / 1000, (info.driver_version % 1000) / 100);
apds9960_interface_debug_print("apds9960: min supply voltage is %0.1fV.\n", info.supply_voltage_min_v);
apds9960_interface_debug_print("apds9960: max supply voltage is %0.1fV.\n", info.supply_voltage_max_v);
apds9960_interface_debug_print("apds9960: max current is %0.2fmA.\n", info.max_current_ma);
apds9960_interface_debug_print("apds9960: max temperature is %0.1fC.\n", info.temperature_max);
apds9960_interface_debug_print("apds9960: min temperature is %0.1fC.\n", info.temperature_min);
}
/* start read test */
apds9960_interface_debug_print("apds9960: start read test.\n");
/* init the apds9960 */
res = apds9960_init(&gs_handle);
if (res != 0)
{
apds9960_interface_debug_print("apds9960: init failed.\n");
return 1;
}
/* power on */
res = apds9960_set_conf(&gs_handle, APDS9960_CONF_POWER_ON, APDS9960_BOOL_TRUE);
/* 代码太长了,这里仅截取一部分代码:以下都是设置 APDS9960 初始化参数 */
}
APDS9960 读取数值任务
此任务每隔1秒钟读取一次传感器数值,并保存到全局变量中。
static void apds9960_task_entry(void *arg)
{
uint8_t res;
uint8_t proximity;
uint16_t red, green, blue, clear;
while (1) {
/* read rgbc */
res = apds9960_read_rgbc(&gs_handle, (uint16_t *)&red, (uint16_t *)&green, (uint16_t *)&blue, (uint16_t *)&clear);
if (res != 0)
{
apds9960_interface_debug_print("apds9960: read rgbc failed.\n");
break;
}
/* read proximity */
res = apds9960_read_proximity(&gs_handle, (uint8_t *)&proximity);
if (res != 0)
{
apds9960_interface_debug_print("apds9960: read proximity failed.\n");
break;
}
/* 拷贝数值 */
mVal.red = red;
mVal.green = green;
mVal.blue = blue;
mVal.clear = clear;
mVal.proximity = proximity;
#if 0
/* output */
apds9960_interface_debug_print("apds9960: red is 0x%04X.\n", red);
apds9960_interface_debug_print("apds9960: green is 0x%04X.\n", green);
apds9960_interface_debug_print("apds9960: blue is 0x%04X.\n", blue);
apds9960_interface_debug_print("apds9960: clear is 0x%04X.\n", clear);
apds9960_interface_debug_print("apds9960: proximity is 0x%02X.\n", proximity);
#endif
vTaskDelay(pdMS_TO_TICKS(1000));
}
(void)apds9960_deinit(&gs_handle);
vTaskDelete(NULL);
}
TouchPad 任务
函数 tsi_trigger_task_create() 仅有两行代码,调用 tsi_init() 初始化 TSI 模块,然后创建任务 tsi_software_trigger() 每隔一段时间读取 TouchPad 状态。
TSI 初始化
TSI 模块支持轮询方式、软件触发和硬件定时器触发方式。这里选择软件触发方式。
void tsi_init(void)
{
volatile uint32_t i = 0;
tsi_selfCap_config_t tsiConfig_selfCap;
/* Enables the clk_16k[1] */
CLOCK_SetupClk16KClocking(kCLOCK_Clk16KToVsys);
CLOCK_SetClkDiv(kCLOCK_DivTsiClk, 1);
CLOCK_AttachClk(kCLK_IN_to_TSI);
// NOTE: TSI 管脚已经在 BOARD_InitPins() 中初始化了
/* TSI default hardware configuration for self-cap mode */
TSI_GetSelfCapModeDefaultConfig(&tsiConfig_selfCap);
/* Initialize the TSI */
TSI_InitSelfCapMode(APP_TSI, &tsiConfig_selfCap);
/* Enable noise cancellation function */
TSI_EnableNoiseCancellation(APP_TSI, true);
NVIC_EnableIRQ(TSI0_IRQn);
TSI_EnableModule(APP_TSI, true); /* Enable module */
/********** Calibration Process ***********/
memset((void *)&buffer, 0, sizeof(buffer));
TSI_SelfCapCalibrate(APP_TSI, &buffer);
/* Print calibrated counter values */
for (i = 0; i < FSL_FEATURE_TSI_CHANNEL_COUNT; i++)
{
// PRINTF("Calibrated counters for channel %d is : %d \r\n", i, buffer.calibratedData[i]);
}
/* 使能软件触发方式 */
TSI_EnableInterrupts(APP_TSI, kTSI_GlobalInterruptEnable);
TSI_EnableInterrupts(APP_TSI, kTSI_EndOfScanInterruptEnable);
TSI_ClearStatusFlags(APP_TSI, kTSI_EndOfScanFlag);
TSI_SetSelfCapMeasuredChannel(APP_TSI, BOARD_TSI_ELECTRODE_1);
}
TSI 读取
因为是软件出发方式,任务中只是开启触发,实际读取结果是在 TSI 中断中。
void tsi_software_trigger(void *arg)
{
while (1) {
// 启动软件触发方式
while (s_tsiInProgress)
{
TSI_StartSoftwareTrigger(APP_TSI);
}
s_tsiInProgress = true;
// PRINTF("Channel %d Normal mode counter is: %d \r\n", BOARD_TSI_ELECTRODE_1, TSI_GetCounter(APP_TSI));
vTaskDelay(pdMS_TO_TICKS(20));
}
}
void TSI0_IRQHandler(void)
{
#if BOARD_TSI_ELECTRODE_1 > 15
/* errata ERR051410: When reading TSI_COMFIG[TSICH] bitfield, the upper most bit will always be 0. */
if ((TSI_GetSelfCapMeasuredChannel(APP_TSI) | 0x10U) == BOARD_TSI_ELECTRODE_1)
#else
if (TSI_GetSelfCapMeasuredChannel(APP_TSI) == BOARD_TSI_ELECTRODE_1)
#endif
{
if (TSI_GetCounter(APP_TSI) > (uint16_t)(buffer.calibratedData[BOARD_TSI_ELECTRODE_1] + TOUCH_DELTA_VALUE))
{
m_TsiStatus = TSI_STATUS_TOUCHED;
m_tsiTrigTick = xTaskGetTickCount();
s_tsiInProgress = false;
}
}
/* Clear endOfScan flag */
TSI_ClearStatusFlags(APP_TSI, kTSI_EndOfScanFlag);
SDK_ISR_EXIT_BARRIER;
}
网络任务
最后最复杂的就是网络任务。它先初始化以太网接口,启动 LwIP 协议栈,创建 tcpip_thread, 然后等待 DHCP 获取IP地址,最后才是启动 UDP 通信任务。
UDP 通信任务才是本次项目的重点,业务逻辑在此。其他几个以太网任务都只是为了初始化网络,成功获取IP地址,不是重点。
void network_task_create(void)
{
xDHCPReadySemaphore = xSemaphoreCreateBinary();
if (NULL == xDHCPReadySemaphore) {
LWIP_ASSERT("Failed to create DHCP ready semaphore!!!\r\n", 0);
}
if (sys_thread_new("enet_init", stack_init, NULL,
INIT_THREAD_STACKSIZE, INIT_THREAD_PRIO) == NULL) {
LWIP_ASSERT("enet_init: task create failed.\r\n", 0);
}
// 创建一个网络任务,但是先要等待 DHCP 成功获取 IP 地址,然后通过 UDP 多播通信
if (xTaskCreate(udp_communicate_task, "UDP-Task",
MY_NETWORK_TASK_STACK_SIZE, NULL,
MY_NETWORK_TASK_PRIORITY, NULL) == pdFALSE) {
LWIP_ASSERT("Failed to create UDP-Task!!!\r\n", 0);
}
}
UDP 通信任务主体
- 等待 IP 地址获取成功;
- 创建 UDP 套接字,绑定到本地端口;
- 设置服务器IP地址和端口号(即电脑的IP地址和端口号,这里为了简便,硬编码);
- 然后调用
reprot_xxx()上报各个模块的信息; - 接收 UDP 报文,调用
dispatch_ctrl_msg()操作执行器类的设备执行动作,例如 RGB LED 点亮和熄灭,电机转动和停止;
/**
* @brief UDP 通信任务,和主机通信
*
* @param pvParameters
*/
void udp_communicate_task(void *pvParameters)
{
err_t err;
// 服务器地址信息
m_ServerAddr.sin_family = AF_INET;
m_ServerAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
m_ServerAddr.sin_port = htons(SERVER_PORT);
// 本地地址信息
m_LocalAddr.sin_family = AF_INET;
m_LocalAddr.sin_addr.s_addr = INADDR_ANY;
m_LocalAddr.sin_port = htons(MY_NETWORK_LOCAL_PORT);
// 任务一开始,先等待 DHCP 成功获取 IP 地址
for (;;) {
if (xSemaphoreTake(xDHCPReadySemaphore, portMAX_DELAY) == pdTRUE) {
// 推迟 500ms 再初始化应用
vTaskDelay(pdMS_TO_TICKS(500));
// 成功获取 IP 地址
PRINTF("Succeed to get IP \r\n");
// 创建 UDP socket
m_UdpSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (m_UdpSocket < 0) {
// 创建 UDP socket 失败
PRINTF("Failed to create UDP socket!!!\r\n");
// TODO:
}
// 绑定到本地端口
if (bind(m_UdpSocket, (struct sockaddr *)&m_LocalAddr, sizeof(struct sockaddr)) == -1) {
PRINTF("Failed to bind port = %u \r\n", MY_NETWORK_LOCAL_PORT);
// TODO:
}
// 设置一个结构体,作为全局的参数
m_PeerInfo.local_sock = m_UdpSocket;
m_PeerInfo.peer_addr = &m_ServerAddr;
m_PeerInfo.sock_len = addr_len;
// 设置接收超时时间
struct timeval sock_recv_timeout =
{
.tv_sec = 0, // 秒
.tv_usec = 1000 * 200 // 微秒
};
if (setsockopt(m_UdpSocket, SOL_SOCKET, SO_RCVTIMEO, &sock_recv_timeout, sizeof(sock_recv_timeout)) == -1) {
PRINTF("Failed to set socket recv timeout\r\n");
// TODO: 错误处理
}
while (1) { // 收发数据
// 先发送
report_temperature( &m_PeerInfo ); // 上报温度信息
report_btn_info(&m_PeerInfo); // 上报按键信息
report_led_info(&m_PeerInfo); // 上报 LED 信息
report_tsi_info(&m_PeerInfo); // 上报 TSI 信息
report_apds9960_info(&m_PeerInfo); // 上报 APDS9960 测量信息
// 再接收
memset(m_RecvBuffer, 0, RECV_BUF_LEN);
int bytes_received = recvfrom(m_UdpSocket, m_RecvBuffer, sizeof(m_RecvBuffer), 0, (struct sockaddr *)&m_TempAddr, &addr_len);
if (bytes_received > 0) {
// 处理接收到的数据
PRINTF("UDP RECV| %s:%d | %s \r\n", inet_ntoa(m_TempAddr.sin_addr), ntohs(m_TempAddr.sin_port), m_RecvBuffer);
dispatch_ctrl_msg((const char *)m_RecvBuffer, bytes_received);
}
}
vTaskDelay(pdMS_TO_TICKS(10));
}
vTaskDelay(pdMS_TO_TICKS(500));
}
}
这里以上报温度信息为例,贴上代码。
/**
* @brief 上报温度的格式为 RPT:TEMP:%f
*/
static void report_temperature(my_network_peer_t *peer)
{
double tempC = 0.0f;
char msg[16] = {0};
tempC = temperature_read();
sprintf(msg, "RPT:TEMP:%6.2f\r\n", tempC);
sendto(peer->local_sock, msg, strlen(msg), 0, (const struct sockaddr *)peer->peer_addr, peer->sock_len);
}
每一种传感器上报信息都有各自的格式,参见代码。
执行器类的执行函数参见 dispatch_ctrl_msg() 函数。比较接收到的信息并执行对应的函数。重点在结构体 m_CtrlMsgProcess。
/**
* @brief 解析服务器发送的 CTRL:xxx 命令,并交给底层去执行
*
* @param msg
* @param length
* @return int 成功返回0;失败返回-1
*/
static int dispatch_ctrl_msg(const char *msg, int length)
{
int retval = -1; // 默认失败
char *p;
// 参数检查
if ((NULL == msg) || (0 == length)) {
PRINTF("Empty Ctrl Msg!!!\r\n");
return -1;
}
if (length >= CTRL_MSG_LEN) { // 如果消息太长了,截断
length = CTRL_MSG_LEN;
}
// 先缓存消息
memset((void *)m_CtrlMsgBuffer, 0, CTRL_MSG_LEN);
memcpy((void *)m_CtrlMsgBuffer, (const void *)msg, length);
// 开始解析消息
for (int i = 0; i < ARRAY_SIZE(m_CtrlMsgProcess); i++) {
ctrl_msg_process_t msg_p = m_CtrlMsgProcess[i];
if (strncmp(m_CtrlMsgBuffer, msg_p.msg, strlen(msg_p.msg)) == 0) {
msg_p.hdl();
retval = 0;
break;
}
}
return retval;
}
static ctrl_msg_process_t m_CtrlMsgProcess[] = {
{
.msg = "CTRL:LEDR:ON",
.hdl = ctrl_ledr_on
},
{
.msg = "CTRL:LEDR:OFF",
.hdl = ctrl_ledr_off
},
{
.msg = "CTRL:LEDG:ON",
.hdl = ctrl_ledg_on
},
{
.msg = "CTRL:LEDG:OFF",
.hdl = ctrl_ledg_off
},
{
.msg = "CTRL:LEDB:ON",
.hdl = ctrl_ledb_on
},
{
.msg = "CTRL:LEDB:OFF",
.hdl = ctrl_ledb_off
},
{
.msg = "CTRL:MOTOR:STOP",
.hdl = ctrl_motor_stop
},
{
.msg = "CTRL:MOTOR:FORWARD",
.hdl = ctrl_motor_forward
},
{
.msg = "CTRL:MOTOR:BACKWARD",
.hdl = ctrl_motor_backward
}
};
2. 上位机软件流程图
上位机是以 Qt 编写的,拖拽生成 UI 布局并绑定各个控件的回调函数。
接收到 UDP 消息,解析并更新对应的控件状态和数值。
用户与 GUI 交互,则发送控制命令给板卡,让板卡执行动作。
Qt 编写的界面如下:
功能展示及说明
板卡实物如下
功能演示参见网址:Funpack3_4-MCXN947-哔哩哔哩
对本活动的心得体会
此次活动收获挺多,从完全不熟悉 NXP 开发环境到熟练的搭建 MCUXpress IDE 环境,安装 MCUXpress for VSCode 开发环境,再到使用 MCUXpresso Config Tools 配置管脚,每一个都是新知识。
NXP 的 MCX 系列 MCU 不同于某32,管脚配置和时钟配置要更复杂,幸亏有例程可以参考,不然无从下手。幸好 NXP 的工具团队也很强大,推出了强大的 IDE 和配置工具,帮助用户手上。
此外勇敢试错、进入 debug 模式可快速的定位和解决问题,是推进项目前进的助推器。
一点点小小的建议:同一个厂商的不同系列的板卡可以连续推几期,巩固对厂商各种IP的理解,熟悉厂商不同产品线的应用场景。