Funpack3-4
——任务二:基于MCX-N947的局域网控制平台
一、项目介绍
本项目依托于Funpack3-4活动,采用MCX-N947作为主控,可以读取mcu的GPIO状态,板载的触摸按键的状态,板载的温度传感器的数据,并控制板载的三色LED的开关。
项目内容:
1、开发一全平台支持的上位机。
2、远程获取板载按键状态,并识别按键次数。
3、远程获取板载触摸按键的状态。
4、使该板卡完成局域网通信。
5、远程控制三色LED。
应用场景
本项目主体依托于路由器产生的局域网进行基于TCP/IP协议的通信。基于该种通信方式,可以实现高可靠性及高通信距离,并能在多种环境下操作设备。如,在该网络中添加单次光纤转接,即可将通信距离延长至20km,可以完成矿井内等恶劣危险环境下的无人操作。如果配备内网穿透服务器,还可以将该设备布置在任意有网络信号的位置,并通过公网的服务器访问该设备,大大降低部署该设备的网络需求。
二、总体架构
图2.1
如图2.1,项目主要分为三个部分,即局域网、控制端和受控端。
本项目中,受控端无疑为FRDM-MCXN947板卡。
目前较为主流的控制端方案多种多样,考虑到编程简单、全平台通用、性能较好、体积较精简等因素,采用QT作为控制端的基本框架。
此处为方便调试,使用路由器开启DHCP,来构建整个局域网,相较控制端和受控端直连,该方式相对稳定,并且设置简单,具有较强的鲁棒性。
三、受控端架构
受控端为FRDM-MCXN947板卡,其大致的硬件资源如下。
软件方面,采用裸机编程,并未运行RTOS系统,使用定时器中断及网络接收中断等来调度系统。编程环境为MCUXpresso IDE,大量使用内置的配置工具初始化代码,可以极大的精简主函数代码复杂度。主要使用一个定时器中断定期控制系统通过网络播报当前系统状态,网络接收中断来接收控制指令。主要代码如下。
主程序
以下为主程序代码,主程序主要包括初始化和主循环。初始化中将温度传感器、LWIP、及触摸的TSI V6外设分别初始化,其中也包含IDE生成的PIN生成和时钟配置。主循环包含三部分,一是LWIP的循环函数,二是循环获取温度传感器数据,三是循环执行四个定时器任务,完成本机的状态发送。
int main(void)
{
volatile uint32_t i = 0;
tsi_selfCap_config_t tsiConfig_selfCap;
lptmr_config_t lptmrConfig;
memset((void *)&lptmrConfig, 0, sizeof(lptmrConfig));
status_t result = kStatus_Success;
i3c_master_config_t masterConfig;
p3t1755_config_t p3t1755Config;
struct netif netif;
ethernetif_config_t enet_config = {.phyHandle = &phyHandle,
.phyAddr = EXAMPLE_PHY_ADDRESS,
.phyOps = EXAMPLE_PHY_OPS,
.phyResource = EXAMPLE_PHY_RESOURCE,
#ifdef configMAC_ADDR
.macAddress = configMAC_ADDR
#endif
};
CLOCK_EnableClock(kCLOCK_InputMux);
BOARD_InitBootPins();
BOARD_InitBootClocks();
BOARD_InitDebugConsole();
BOARD_InitBootPeripherals();
CLOCK_AttachClk(MUX_A(CM_ENETRMIICLKSEL, 0));
CLOCK_EnableClock(kCLOCK_Enet);
SYSCON0->PRESETCTRL2 = SYSCON_PRESETCTRL2_ENET_RST_MASK;
SYSCON0->PRESETCTRL2 &= ~SYSCON_PRESETCTRL2_ENET_RST_MASK;
MDIO_Init();
g_phy_resource.read = MDIO_Read;
g_phy_resource.write = MDIO_Write;
time_init();
/* Set MAC address. */
#ifndef configMAC_ADDR
(void)SILICONID_ConvertToMacAddr(&enet_config.macAddress);
#endif
/* Get clock after hardware init. */
enet_config.srcClockHz = EXAMPLE_CLOCK_FREQ;
lwip_init();
netif_add_ext_callback(&linkStatusCallbackInfo, linkStatusCallback);
netif_add(&netif, NULL, NULL, NULL, &enet_config, EXAMPLE_NETIF_INIT_FN, ethernet_input);
netif_set_default(&netif);
netif_set_up(&netif);
while (ethernetif_wait_linkup(&netif, 5000) != ERR_OK)
{
PRINTF("PHY Auto-negotiation failed. Please check the cable connection and link partner setting.\r\n");
}
dhcp_start(&netif);
PRINTF("\r\n************************************************\r\n");
PRINTF(" DHCP example\r\n");
PRINTF("************************************************\r\n");
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);
result = p3t1755_set_dynamic_address();
if (result != kStatus_Success)
{
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);
TCP_Echo_Init();
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 */
memset((void *)&buffer, 0, sizeof(buffer));
TSI_SelfCapCalibrate(APP_TSI, &buffer);
TSI_EnableModule(APP_TSI, false);
TSI_EnableHardwareTriggerScan(APP_TSI, true);
TSI_EnableInterrupts(APP_TSI, kTSI_EndOfScanInterruptEnable);
TSI_ClearStatusFlags(APP_TSI, kTSI_EndOfScanFlag);
TSI_SetSelfCapMeasuredChannel(APP_TSI,BOARD_TSI_ELECTRODE_1); /* Select BOARD_TSI_ELECTRODE_1 as detecting electrode. */
TSI_EnableModule(APP_TSI, true);
INPUTMUX_AttachSignal(INPUTMUX0, 0U, kINPUTMUX_Lptmr0ToTsiTrigger);
LPTMR_StartTimer(LPTMR0); /* Start LPTMR triggering */
while (1)
{
/* Poll the driver, get any outstanding frames */
ethernetif_input(&netif);
/* Handle all system timeouts for all core protocols */
sys_check_timeouts();
/* Print DHCP progress */
print_dhcp_state(&netif);
result = P3T1755_ReadTemperature(&p3t1755Handle, &temperature);
switch(tim1_flag)
{
case 1:
tim1_net_task();
tim1_flag=0;
break;
case 2 :
tim2_net_task();
tim1_flag=0;
break;
case 3 :
tim3_net_task();
tim1_flag=0;
break;
case 4 :
tim4_net_task();
tim1_flag=0;
break;
default:
tim1_flag=0;
}
}
}
定时器中断
本项目仅启用了一个定时器中断,采用状态机思想,将循环改变系统发送任务的控制位,以达到调度的目的。
void tim1_match0_IRQ(uint32_t flags)
{
switch(tim2_flag)
{
case 1:
tim1_flag=1;
tim2_flag+=1;
break;
case 2:
tim1_flag=2;
tim2_flag+=1;
break;
case 3:
tim1_flag=3;
tim2_flag+=1;
break;
case 4:
tim1_flag=4;
tim2_flag+=1;
break;
default:
tim2_flag=1;
}
}
系统状态发送
本项目有四个系统状态发送函数,用于发送不同的参数状态,其流程大致相同,都是遍历每一个活跃的连接,逐个发送某一参数状态,代码如下。
void tim1_net_task()
{
struct tcp_pcb *pcb;
for (pcb = tcp_active_pcbs; pcb != NULL; pcb = pcb->next)
{
sprintf(output_buf,"s");
tcp_write(pcb, output_buf,1, 1);
sprintf(output_buf,"%d\n",touch_state);
tcp_write(pcb, output_buf,2, 1);
}
}
void tim2_net_task()
{
struct tcp_pcb *pcb;
for (pcb = tcp_active_pcbs; pcb != NULL; pcb = pcb->next)
{
sprintf(output_buf,"t");
tcp_write(pcb, output_buf,1, 1);
sprintf(output_buf,"%f\n",temperature);
tcp_write(pcb, output_buf, sizeof(output_buf), 1);
}
}
void tim3_net_task()
{
struct tcp_pcb *pcb;
for (pcb = tcp_active_pcbs; pcb != NULL; pcb = pcb->next)
{
sprintf(output_buf,"i");
tcp_write(pcb, output_buf,1, 1);
sprintf(output_buf,"%d\n",GPIO_PinRead(GPIO0, 6));
tcp_write(pcb, output_buf,2, 1);
}
}
void tim4_net_task()
{
struct tcp_pcb *pcb;
for (pcb = tcp_active_pcbs; pcb != NULL; pcb = pcb->next)
{
sprintf(output_buf,"w");
tcp_write(pcb, output_buf,1, 1);
sprintf(output_buf,"%d\n",GPIO_PinRead(GPIO0, 23));
tcp_write(pcb, output_buf,2, 1);
}
}
TCP接收中断函数
TCP接收函数主要分为两步,首先复制接收到的数据,然后释放原变量占用的内存,然后根据接收到的数据控制系统产生相应的动作。
static err_t tcpecho_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
{
struct pbuf *q;
uint32_t data_len=0;
int dac_value=0;
if (p != NULL)
{
/* 更新窗口*/
tcp_recved(tpcb, p->tot_len);
for(q=p;q!=NULL;q=q->next)
{
memcpy(receive_buf+data_len,q->payload,q->tot_len);
data_len+=q->tot_len;
}
tcp_write(tpcb, p->payload, 1, 1);
memset(p->payload, 0 , p->tot_len);
pbuf_free(p);
switch(receive_buf[0]){
case 'l':
switch(receive_buf[1]){
case 'b':
if(receive_buf[2]=='1')
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);
break;
case 'g':
if(receive_buf[2]=='1')
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);
break;
case 'r':
switch(receive_buf[2])
{
case '1':
GPIO_PortClear(BOARD_LED_RED_GPIO, 1u << BOARD_LED_RED_GPIO_PIN);
break;
case'0':
GPIO_PortSet(BOARD_LED_RED_GPIO, 1u << BOARD_LED_RED_GPIO_PIN);
break;
}
break;
}
break;
default:
}
}
else if (err == ERR_OK)
{
return tcp_close(tpcb);
}
return ERR_OK;
}
四、控制端架构
控制端采用QT作为框架,使用双界面分别控制连接参数和系统监控,降低开发难度,使界面更美观。本章将先行描述各主要函数,最后描述界面优化相关函数。
4.1 连接界面
得益于QT良好的框架设计,控制界面主函数无重要代码,完全采用事件驱动式编程,当连接按键按下时,会向受控端发起连接,如果失败会弹出错误弹窗,成功则会关闭当前界面,打开控制界面。代码如下。
void MainWindow::on_pushButton_clicked()
{
mSocket=new QTcpSocket(this);
QObject::connect(mSocket,&QTcpSocket::connected,this,[&](){
GSocket=mSocket;
ControlWindow *cw=new ControlWindow;
cw->show();
this->close();
});
QString ip=ui->lineEdit->text();
quint16 port=ui->spinBox->value();
mSocket->connectToHost(ip,port);
if(!mSocket->waitForConnected(5000))
{
QMessageBox::warning(this, QObject::tr("连接电源"), QObject::tr("连接失败"));
}
}
4.2 控制界面
控制界面也是事件驱动式编程,初始化时需要绑定部分响应函数,其余可由QTCreator图形化绑定。受控端状态更新完全由初始化时绑定的函数控制,对受控端的控制完全由图形化绑定的函数完成。
4.2.1初始化
初始化部分主要是编写TCP/IP的接收函数,考虑到QT触发readyread信号具有随机性,且当前可以忍受丢失少数数据,因此将接收到的数据分为几类,如果是有效的头,就执行相应的操作,如果为无效的头,就抛弃该数据,直到找到有效的数据头。在执行完有效的操作后,会尝试再次读取,如果不为空,则跳转至头部;若为空,刷新缓存。
ControlWindow::ControlWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::ControlWindow)
{
ui->setupUi(this);
QObject::connect(GSocket,&QTcpSocket::readyRead,this,[&](){
re: QByteArray flag = GSocket->read(1);
re1: QString flag_str(flag);
int flag_int=*flag_str.toUtf8().data();
double t=0;
QByteArray arr ;
qDebug()<<"flag:"<<flag;
switch (flag_int)
{
case 'r':
;
break;
case 's':
arr =GSocket->read(1);
t=arr.toInt();
qDebug()<<"touch:"<<t;
if (t)
{
ui->pushButton_6->setStyleSheet("background-color:rgb(0, 250, 0)");
}
else
{
ui->pushButton_6->setStyleSheet("background-color:rgb(250, 0, 0)");
}
break;
case 'i':
arr =GSocket->read(1);
t=arr.toInt();
qDebug()<<"key_isp:"<<t;
if (!t)
{
ui->pushButton_5->setStyleSheet("background-color:rgb(0, 250, 0)");
}
else
{
ui->pushButton_5->setStyleSheet("background-color:rgb(250, 0, 0)");
}
key_value_i=ui->doubleSpinBox_2->value();
if(t>key_last_i)
ui->doubleSpinBox_2->setValue(key_value_i+1);
key_last_i=t;
break;
case 'w':
arr =GSocket->read(1);
t=arr.toInt();
qDebug()<<"key_WAKEUP:"<<t;
if (!t)
{
ui->pushButton_7->setStyleSheet("background-color:rgb(0, 250, 0)");
}
else
{
ui->pushButton_7->setStyleSheet("background-color:rgb(250, 0, 0)");
}
key_value_w=ui->doubleSpinBox_3->value();
if(t>key_last_w)
ui->doubleSpinBox_3->setValue(key_value_w+1);
key_last_w=t;
break;
case 't' :
arr =GSocket->read(sizeof(t));
t=arr.toDouble();
ui->doubleSpinBox->setValue(t);
qDebug()<<"Temp:"<<t;
break;
case '\n' :
goto re;
break;
case '0' :
GSocket->read(1);
goto re;
break;
case '1' :
GSocket->read(1);
goto re;
break;
default:
arr =GSocket->readAll();
;
}
flag = GSocket->read(1);
if (flag!="")
goto re1;
arr =GSocket->readAll();
qDebug()<<"other"<<arr;
});
}
4.2.2控制函数
对LED的控制较为简单,根据当前按钮的状态来执行相应的LED开关操作。代码如下。
void ControlWindow::on_pushButton_2_clicked(bool checked)//IO1
{
if (checked)
{
GSocket->write("lr1");
}
else
{
GSocket->write("lr0");
}
}
void ControlWindow::on_pushButton_3_clicked(bool checked)//IO2
{
if (checked)
{
GSocket->write("lg1");
}
else
{
GSocket->write("lg0");
}
}
void ControlWindow::on_pushButton_4_clicked(bool checked)//IO3
{
if (checked)
{
GSocket->write("lb1");
}
else
{
GSocket->write("lb0");
}
}
4.3 界面优化
为方便用户使用,本程序添加了大量的界面优化函数。比如与受控端连接失败时会有失败的提示弹窗,如下。
if(!mSocket->waitForConnected(5000))
{
QMessageBox::warning(this, QObject::tr("连接电源"), QObject::tr("连接失败"));
}
此外,连接界面和控制界面均有相应的控件大小自适应函数,主要依赖网格排布和控制字体来实现,如下。
//连接界面
void MainWindow::resizeEvent(QResizeEvent *event)
{
int width = this->size().width();
int height = this->size().height();
width/=20;
height/=1;
if(width>height)
{
font_left.setPixelSize(height);
font_mid.setPixelSize(height);
ui->spinBox->setFont(font_left);
ui->lineEdit->setFont(font_left);
ui->label->setFont(font_mid);
ui->label_3->setFont(font_mid);
ui->pushButton->setFont(font_mid);
}
else
{
font_left.setPixelSize(width);
font_mid.setPixelSize(width);
ui->spinBox->setFont(font_left);
ui->lineEdit->setFont(font_left);
ui->label->setFont(font_mid);
ui->label_3->setFont(font_mid);
ui->pushButton->setFont(font_mid);
}
}
//控制界面
void ControlWindow::resizeEvent(QResizeEvent *event)
{
int width = this->size().width();
int height = this->size().height();
width/=15;
height/=15;
if(width>height)
{
font_left.setPixelSize(height);
font_mid.setPixelSize(height);
ui->doubleSpinBox->setFont(font_left);
ui->doubleSpinBox_2->setFont(font_left);
ui->doubleSpinBox_3->setFont(font_left);
ui->label->setFont(font_mid);
ui->label_2->setFont(font_mid);
ui->label_3->setFont(font_mid);
ui->pushButton->setFont(font_mid);
ui->pushButton_2->setFont(font_mid);
ui->pushButton_3->setFont(font_mid);
ui->pushButton_4->setFont(font_mid);
ui->pushButton_5->setFont(font_mid);
ui->pushButton_6->setFont(font_mid);
ui->pushButton_7->setFont(font_mid);
}
else
{
font_left.setPixelSize(width);
font_mid.setPixelSize(width);
ui->doubleSpinBox->setFont(font_left);
ui->doubleSpinBox_2->setFont(font_left);
ui->doubleSpinBox_3->setFont(font_left);
ui->label->setFont(font_mid);
ui->label_2->setFont(font_mid);
ui->label_3->setFont(font_mid);
ui->pushButton->setFont(font_mid);
ui->pushButton_2->setFont(font_mid);
ui->pushButton_3->setFont(font_mid);
ui->pushButton_4->setFont(font_mid);
ui->pushButton_5->setFont(font_mid);
ui->pushButton_6->setFont(font_mid);
ui->pushButton_7->setFont(font_mid);
}
}
最后,还设置了QT内置但未设置的应用主题,使上位机会自适应浅色/深色模式并具有更好的质感,如下。
a.setStyle(QStyleFactory::create("fusion"));
五、最终效果
具体效果请参考视频,此处放出上位机截图。