為了讓RobomasterC板(這塊板用的是STM32F407IGHX的芯片)能與上位機(jī)進(jìn)行通訊。我最近翻了不少博客和CSDN文章,看到了很多文章存在一些問(wèn)題,經(jīng)過(guò)了一下午試錯(cuò),我成功實(shí)現(xiàn)了STM32F407IGHX利用STM32CubeIDE進(jìn)行配置并然后用HAL庫(kù)進(jìn)行編程,與安裝有ROS的Ubuntu進(jìn)行虛擬串口通信。
在翻看博客的時(shí)候我發(fā)現(xiàn),RM以及上下位機(jī)通信資料并不多,而且很多已有資料都只講述了實(shí)現(xiàn)原理,卻沒(méi)有講如何具體一步步實(shí)現(xiàn)某個(gè)功能,這就導(dǎo)致初學(xué)者可能在翻看過(guò)程中,越看越懵,反而寫(xiě)不出一份能用的代碼。
所以這篇文章會(huì)盡可能詳細(xì)的講怎么實(shí)現(xiàn)串口通信,而盡量少講其原理,由于很多文章都已經(jīng)詳盡的寫(xiě)出了串口通信的原理了,所以我就不在贅述原理而著重于實(shí)現(xiàn)過(guò)程。
此外,我也會(huì)把一些小問(wèn)題和建議寫(xiě)出來(lái),以便一篇文章就解決所有可能存在的問(wèn)題。
一、概述
1、STM32端(所謂的下位機(jī)):這邊采用的是通過(guò)有圖形化的STM32CubeIDE配置工程,配置好USB-CDC創(chuàng)建一個(gè)虛擬串口,與上位機(jī)通信。
2、Ubuntu端(所謂的上位機(jī)):上位機(jī)是版本20.04的ubuntu,安裝有版本為noetic的ROS,通過(guò)建立一個(gè)ROS節(jié)點(diǎn)來(lái)打開(kāi)串口并建立通信。
二、STM32端具體實(shí)現(xiàn)過(guò)程
思路:利用STM32CubeIDE配置好USB-CDC,接著修改對(duì)應(yīng)的頭文件,自定義所需的函數(shù)。
1、配置過(guò)程
1)先配置時(shí)鐘RCC,設(shè)置高速時(shí)鐘High Speed Clock為內(nèi)部時(shí)鐘(Crystal/Ceramic Resonator),另一個(gè)暫時(shí)用不到所以不設(shè)置。
2)配置下載與調(diào)試(必須設(shè)置,否則會(huì)鎖芯片,到時(shí)候還需要通過(guò)BOOT重啟,比較麻煩)
設(shè)置為Serial Wire,時(shí)鐘為SysTick(當(dāng)然看你到底有什么,如果你擁有的是ST-LINK,那么可以這樣設(shè)置)
3)設(shè)置USB模式,打開(kāi)Connectivity,選擇USB-OYG-FS(快速),選擇Mode的Device_only(從機(jī)模式)。然后點(diǎn)開(kāi)左下方的NVIC Settings,勾選Enabled,從而能夠開(kāi)啟中斷。
備注:還要返回到NVIC中,設(shè)置USB中斷的優(yōu)先級(jí),這里設(shè)置個(gè)4就行(畢竟沒(méi)有啟動(dòng)其他外設(shè),所以中斷就不需要太嚴(yán)謹(jǐn))、
4)打開(kāi)MiddleWare,設(shè)置USB的具體工作方式,選擇Class For FS IP的Communication Device Class,即VCP(虛擬串口),其余設(shè)置保持默認(rèn)即可,不需要額外修改。
5)時(shí)鐘樹(shù)設(shè)置(時(shí)鐘樹(shù)的設(shè)置,需要查閱所使用開(kāi)發(fā)板的具體原理圖)
例如,RobomasterC板原理圖里是如此說(shuō)明的,所以Input frequency要設(shè)置成12MHz。此外,下方畫(huà)紅線部分是USB的時(shí)鐘,USB的時(shí)鐘需要設(shè)置成48MHz才能工作,其余部分看自己的需求。
6)堆棧設(shè)置,堆棧的大小需要足夠大,才能滿足USB初始化的需求,此處設(shè)置Heap Size為0X600即可解決初始化失敗的問(wèn)題,另一個(gè)不用改。
7)到此,所有的初始化已經(jīng)結(jié)束了,只需要Ctrl+s,保存并生成代碼即可,下方兩個(gè)選項(xiàng)均選擇Yes,即可生成STM32CubeIDE工程
2、代碼的修改
這里要先打開(kāi)工程里的USB_DEVICE中的App的usbd_cdc_if.c,重構(gòu)官方給出的代碼,具體內(nèi)容如下
/* USER CODE BEGIN Header */ /** ****************************************************************************** * @file : usbd_cdc_if.c * @version : v1.0_Cube * @brief : Usb device for Virtual Com Port. ****************************************************************************** * @attention * * Copyright (c) 2023 STMicroelectronics. * All rights reserved. * * This software is licensed under terms that can be found in the LICENSE file * in the root directory of this software component. * If no LICENSE file comes with this software, it is provided AS-IS. * ****************************************************************************** */ /* USER CODE END Header */ /* Includes ------------------------------------------------------------------*/ #include "usbd_cdc_if.h" /* USER CODE BEGIN INCLUDE */ /* USER CODE END INCLUDE */ /* Private typedef -----------------------------------------------------------*/ /* Private define ------------------------------------------------------------*/ /* Private macro -------------------------------------------------------------*/ /* USER CODE BEGIN PV */ /* Private variables ---------------------------------------------------------*/ /* USER CODE END PV */ /** @addtogroup STM32_USB_OTG_DEVICE_LIBRARY * @brief Usb device library. * @{ */ /** @addtogroup USBD_CDC_IF * @{ */ /** @defgroup USBD_CDC_IF_Private_TypesDefinitions USBD_CDC_IF_Private_TypesDefinitions * @brief Private types. * @{ */ /* USER CODE BEGIN PRIVATE_TYPES */ /* USER CODE END PRIVATE_TYPES */ /** * @} */ /** @defgroup USBD_CDC_IF_Private_Defines USBD_CDC_IF_Private_Defines * @brief Private defines. * @{ */ /* USER CODE BEGIN PRIVATE_DEFINES */ /* USER CODE END PRIVATE_DEFINES */ /** * @} */ /** @defgroup USBD_CDC_IF_Private_Macros USBD_CDC_IF_Private_Macros * @brief Private macros. * @{ */ /* USER CODE BEGIN PRIVATE_MACRO */ /* USER CODE END PRIVATE_MACRO */ /** * @} */ /** @defgroup USBD_CDC_IF_Private_Variables USBD_CDC_IF_Private_Variables * @brief Private variables. * @{ */ /* Create buffer for reception and transmission */ /* It's up to user to redefine and/or remove those define */ /** Received data over USB are stored in this buffer */ uint8_t UserRxBufferFS[APP_RX_DATA_SIZE]; /** Data to send over USB CDC are stored in this buffer */ uint8_t UserTxBufferFS[APP_TX_DATA_SIZE]; /* USER CODE BEGIN PRIVATE_VARIABLES */ /* USER CODE END PRIVATE_VARIABLES */ /** * @} */ /** @defgroup USBD_CDC_IF_Exported_Variables USBD_CDC_IF_Exported_Variables * @brief Public variables. * @{ */ extern USBD_HandleTypeDef hUsbDeviceFS; /* USER CODE BEGIN EXPORTED_VARIABLES */ /* USER CODE END EXPORTED_VARIABLES */ /** * @} */ /** @defgroup USBD_CDC_IF_Private_FunctionPrototypes USBD_CDC_IF_Private_FunctionPrototypes * @brief Private functions declaration. * @{ */ static int8_t CDC_Init_FS(void); static int8_t CDC_DeInit_FS(void); static int8_t CDC_Control_FS(uint8_t cmd, uint8_t* pbuf, uint16_t length); static int8_t CDC_Receive_FS(uint8_t* pbuf, uint32_t *Len); static int8_t CDC_TransmitCplt_FS(uint8_t *pbuf, uint32_t *Len, uint8_t epnum); /* USER CODE BEGIN PRIVATE_FUNCTIONS_DECLARATION */ /* USER CODE END PRIVATE_FUNCTIONS_DECLARATION */ /** * @} */ USBD_CDC_ItfTypeDef USBD_Interface_fops_FS = { CDC_Init_FS, CDC_DeInit_FS, CDC_Control_FS, CDC_Receive_FS, CDC_TransmitCplt_FS }; /* Private functions ---------------------------------------------------------*/ /** * @brief Initializes the CDC media low layer over the FS USB IP * @retval USBD_OK if all operations are OK else USBD_FAIL */ static int8_t CDC_Init_FS(void) { /* USER CODE BEGIN 3 */ /* Set Application Buffers */ USBD_CDC_SetTxBuffer(&hUsbDeviceFS, UserTxBufferFS, 0); USBD_CDC_SetRxBuffer(&hUsbDeviceFS, UserRxBufferFS); return (USBD_OK); /* USER CODE END 3 */ } /** * @brief DeInitializes the CDC media low layer * @retval USBD_OK if all operations are OK else USBD_FAIL */ static int8_t CDC_DeInit_FS(void) { /* USER CODE BEGIN 4 */ return (USBD_OK); /* USER CODE END 4 */ } /** * @brief Manage the CDC class requests * @param cmd: Command code * @param pbuf: Buffer containing command data (request parameters) * @param length: Number of data to be sent (in bytes) * @retval Result of the operation: USBD_OK if all operations are OK else USBD_FAIL */ static int8_t CDC_Control_FS(uint8_t cmd, uint8_t* pbuf, uint16_t length) { /* USER CODE BEGIN 5 */ switch(cmd) { case CDC_SEND_ENCAPSULATED_COMMAND: break; case CDC_GET_ENCAPSULATED_RESPONSE: break; case CDC_SET_COMM_FEATURE: break; case CDC_GET_COMM_FEATURE: break; case CDC_CLEAR_COMM_FEATURE: break; /*******************************************************************************/ /* Line Coding Structure */ /*-----------------------------------------------------------------------------*/ /* Offset | Field | Size | Value | Description */ /* 0 | dwDTERate | 4 | Number |Data terminal rate, in bits per second*/ /* 4 | bCharFormat | 1 | Number | Stop bits */ /* 0 - 1 Stop bit */ /* 1 - 1.5 Stop bits */ /* 2 - 2 Stop bits */ /* 5 | bParityType | 1 | Number | Parity */ /* 0 - None */ /* 1 - Odd */ /* 2 - Even */ /* 3 - Mark */ /* 4 - Space */ /* 6 | bDataBits | 1 | Number Data bits (5, 6, 7, 8 or 16). */ /*******************************************************************************/ case CDC_SET_LINE_CODING: break; case CDC_GET_LINE_CODING: break; case CDC_SET_CONTROL_LINE_STATE: break; case CDC_SEND_BREAK: break; default: break; } return (USBD_OK); /* USER CODE END 5 */ } /** * @brief Data received over USB OUT endpoint are sent over CDC interface * through this function. * * @note * This function will issue a NAK packet on any OUT packet received on * USB endpoint until exiting this function. If you exit this function * before transfer is complete on CDC interface (ie. using DMA controller) * it will result in receiving more data while previous ones are still * not sent. * * @param Buf: Buffer of data to be received * @param Len: Number of data received (in bytes) * @retval Result of the operation: USBD_OK if all operations are OK else USBD_FAIL */ static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len) { /* USER CODE BEGIN 6 */ USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]); USBD_CDC_ReceivePacket(&hUsbDeviceFS); return (USBD_OK); /* USER CODE END 6 */ } /** * @brief CDC_Transmit_FS * Data to send over USB IN endpoint are sent over CDC interface * through this function. * @note * * * @param Buf: Buffer of data to be sent * @param Len: Number of data to be sent (in bytes) * @retval USBD_OK if all operations are OK else USBD_FAIL or USBD_BUSY */ uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len) { uint8_t result = USBD_OK; /* USER CODE BEGIN 7 */ USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData; if (hcdc->TxState != 0){ return USBD_BUSY; } USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len); result = USBD_CDC_TransmitPacket(&hUsbDeviceFS); /* USER CODE END 7 */ return result; } /** * @brief CDC_TransmitCplt_FS * Data transmitted callback * * @note * This function is IN transfer complete callback used to inform user that * the submitted Data is successfully sent over USB. * * @param Buf: Buffer of data to be received * @param Len: Number of data received (in bytes) * @retval Result of the operation: USBD_OK if all operations are OK else USBD_FAIL */ static int8_t CDC_TransmitCplt_FS(uint8_t *Buf, uint32_t *Len, uint8_t epnum) { uint8_t result = USBD_OK; /* USER CODE BEGIN 13 */ if(flag) { CDC_Transmit_FS(UserTxBufferFS, APP_TX_DATA_SIZE); } UNUSED(Buf); UNUSED(Len); UNUSED(epnum); /* USER CODE END 13 */ return result; } /* USER CODE BEGIN PRIVATE_FUNCTIONS_IMPLEMENTATION */ /* USER CODE END PRIVATE_FUNCTIONS_IMPLEMENTATION */ /** * @} */ /** * @} */
注意,不要輕易重新初始化代碼,否則這些對(duì)官方代碼的修改會(huì)被重新覆蓋,導(dǎo)致又要再改一遍,最好一次就初始化好。
3、自定義結(jié)構(gòu)體
在這里我不會(huì)給出具體的代碼,但我會(huì)舉個(gè)例子來(lái)說(shuō)明如何定義所需結(jié)構(gòu)體。
typedef struct ControlData_Chassis _Controldata_Chassis;//這里是定義該結(jié)構(gòu)體的別名 typedef struct ControlData_Chassis{ uint8_t Y_Speed; //縱軸方向速度 uint8_t X_Speed; //橫軸方向速度 uint8_t Rotational_Speed; //小車(chē)旋轉(zhuǎn)速度 uint8_t Chassis_State; //底盤(pán)狀態(tài) }*_Controldata_ChassisInfo;//這里定義了該結(jié)構(gòu)體的結(jié)構(gòu)體指針。C語(yǔ)言允許這樣的操作!
在實(shí)際操作的時(shí)候,可以把這種結(jié)構(gòu)體變量的數(shù)值放入到指定的數(shù)組中(這也就是所謂的打包。而把接收到的數(shù)組中的數(shù)據(jù)按結(jié)構(gòu)體成員形式放入到指定結(jié)構(gòu)體的過(guò)程,就稱之為解包。),從而實(shí)現(xiàn)打包。
此外,可以把結(jié)構(gòu)體定義在頭文件中,便于在.c文件里函數(shù)的具體實(shí)現(xiàn)。
4、自定義解包/打包函數(shù)
這里我也只會(huì)給出一個(gè)例子。
void Pack_Data(_FeedBack* feedback,uint8_t* feedArray) { //把數(shù)組中信息封入數(shù)據(jù)包中 feedArray[0] = 0XFF;//這是幀頭 feedArray[1] = feedback->Shoot_Mode; feedArray[2] = feedback->Shoot_Speed; feedArray[3] = feedback->Armor_Id; feedArray[4] = (uint8_t)(feedback->HP_Remain); feedArray[5] = (uint8_t)(feedback->HP_Remain >> 8); feedArray[6] = 0XAA;//暫時(shí)無(wú)意義 feedArray[7] = 0XFE;//芝士幀尾 }
實(shí)際上,解包函數(shù)也是類似上文的操作,只不過(guò)是反了過(guò)來(lái)。
注:1.可以利用與 “ | ” 來(lái)將兩個(gè)數(shù)據(jù)拼成一個(gè),將拆分的數(shù)據(jù)合成一個(gè)。
2.幀頭和幀尾起到了驗(yàn)證的作用,可以用來(lái)驗(yàn)證數(shù)據(jù)完整性。
5、自定義發(fā)送/接收函數(shù)
int CDC_SendFeed(uint8_t* Fed, uint16_t Len) { CDC_Transmit_FS(Fed, Len); return 0; }
上文調(diào)用了之前修改過(guò)的官方代碼,這樣模塊化的代碼更容易理解與閱讀。
6、備注
1)如果你要定義一個(gè)結(jié)構(gòu)體指針并想給它賦值,那么你需要在賦值前給它分配空間,否則這個(gè)指針無(wú)法進(jìn)行賦值。
例子:
_FeedBack* ft,fd;
ft=(_FeedBack*)malloc(sizeof(_FeedBack));//這里是結(jié)構(gòu)體的空間分配以及具體賦值
三、Ubuntu端具體實(shí)現(xiàn)過(guò)程
思路:利用ROS的serial包來(lái)實(shí)現(xiàn)串口通信。
1、創(chuàng)建工程
此處創(chuàng)建工程,要記得包含roscpp rospy std_msgs 以及serial包(serial包是串口通信的關(guān)鍵)
2、創(chuàng)建主程序
我這里使用的是Visual Studio Code來(lái)編寫(xiě)代碼。
可以先在終端切換到你所需要編寫(xiě)代碼的文件夾,然后輸入 code . (注意后面那個(gè)點(diǎn)也是要輸入的,然后VS就會(huì)啟動(dòng)并打開(kāi)這個(gè)文件夾)。
接著就可以在VS里創(chuàng)建新.cpp文件。
注意:1、如果#include "ros/ros.h"時(shí)發(fā)現(xiàn)找不到所需的頭文件,那么需要修改該工程的配置。按住Shift+Ctrl+P ,即可打開(kāi)配置欄,然后選中第一個(gè)即可。
2、創(chuàng)建好.cpp文件,記得要到CMakeList.txt里添加上該頭文件(其實(shí)只要去掉這三個(gè)語(yǔ)句前的#號(hào),并修改部分內(nèi)容即可,其他部分不用動(dòng))。
add_executable(robo-serial src/robo-serial.cpp)
add_dependencies(robo-serial ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
target_link_libraries(robo-serial
${catkin_LIBRARIES}
)
可以參考下方貼出的代碼來(lái)修改你的配置。
{ "configurations": [ { "browse": { "databaseFilename": "${workspaceFolder}/.vscode/browse.vc.db", "limitSymbolsToIncludedHeaders": false }, "includePath": [ "/home/jinshuai/ros-test/tf_test/devel/include/**", "/opt/ros/noetic/include/**", "/usr/include/**" ], "name": "ROS", "intelliSenseMode": "gcc-x64", "compilerPath": "/usr/bin/gcc", "cStandard": "gnu11", "cppStandard": "c++14" } ], "version": 4 }
我在這里會(huì)給出初始化的大概配置,而具體代碼不會(huì)提供,各位可以參考這個(gè)代碼進(jìn)行修改。
#include "serial/serial.h"http://調(diào)用串口相關(guān)頭文件 #include "ros/ros.h"http://在ros下使用serial包進(jìn)行通訊 #include "iostream" //全局變量定義區(qū) serial::Serial sp;//創(chuàng)建一個(gè)Serial類 serial::Timeout to = serial::Timeout::simpleTimeout(5000);//創(chuàng)建timeout//全局變量定義區(qū) int main(int argc,char** argv){ setlocale(LC_CTYPE,"zh_CN.utf8");//設(shè)置中文輸出 ros::init(argc,argv,"serial_port"); ros::NodeHandle n;//創(chuàng)建句柄 // serial::Serial sp;//創(chuàng)建一個(gè)Serial類 // serial::Timeout to = serial::Timeout::simpleTimeout(5000);//創(chuàng)建timeout sp.setPort("/dev/ttyACM0");//設(shè)置要打開(kāi)的串口名稱 sp.setBaudrate(115200);//設(shè)置串口通信的波特率 sp.setTimeout(to);//串口設(shè)置timeout try { sp.open();//嘗試啟動(dòng)串口 } catch(serial::IOException& e) { ROS_ERROR_STREAM("Unable to open port!Please check your setting!"); return -1; } if(sp.isOpen()) { ROS_INFO_STREAM("/dev/ttyACM0 is opened!");//判斷是否成功開(kāi)啟串口 } else { return -1; } ros::Rate loop_rate(500); while(ros::ok()) { Data_Receive();//此處為自定義函數(shù),不要復(fù)制,我沒(méi)給出具體實(shí)現(xiàn)過(guò)程 Data_Transmit();//此處為自定義函數(shù),不要復(fù)制,我沒(méi)給出具體實(shí)現(xiàn)過(guò)程 loop_rate.sleep(); } sp.close(); return 0; }
3、備注
1)創(chuàng)建結(jié)構(gòu)體,枚舉,打包/解包函數(shù),發(fā)送/接收函數(shù)和STM32端幾乎一樣,所以可以按照STM32端的思路來(lái)操作。但是要注意,上位機(jī)的代碼應(yīng)該是和下位機(jī)相對(duì)應(yīng)的,下位機(jī)接收到的數(shù)據(jù)是來(lái)自上位機(jī)的,所以幀頭幀尾以及結(jié)構(gòu)體成員應(yīng)該保持一致。避免發(fā)送出錯(cuò)。
2)如果你想要在ROS工程里自定義一個(gè)頭文件和C文件,那么記得去修改CMakeList.txt里的
add_executable(robo-serial src/robo-serial.cpp)
add_dependencies(robo-serial Test ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
target_link_libraries(robo-serial Test
${catkin_LIBRARIES}
)
add_library(Serial
src/Test.h
src/Test.cpp
)
否則會(huì)報(bào)錯(cuò),找不到該頭文件。修改方式參考上圖粗體部分。
四、可能存在的報(bào)錯(cuò)
1、如果PC無(wú)法連接到虛擬串口,并顯示“無(wú)法獲取設(shè)備描述符”
我的解決辦法:
1)線路連接不良或者線路有問(wèn)題,建議重新連接或者換一根線(有一定可能)
2)工程配置錯(cuò)誤,時(shí)鐘樹(shù)有誤(需要根據(jù)你的開(kāi)發(fā)板,重新觀察時(shí)鐘樹(shù)的配置。是否引入了正確的時(shí)鐘,以及是否配置好了USB時(shí)鐘(48MHz))
2、Ubuntu無(wú)法打開(kāi)串口
1)連接有問(wèn)題或者根本沒(méi)有連接
2)沒(méi)有權(quán)限打開(kāi)串口(進(jìn)入管理員模式(終端輸入sudo -i),接著編輯/etc/udev/rules.d/70-ttyusb.rules,加上一行KERNEL=="ttyUSB[0-9]*",MODE="0666" 保存退出即可。注意,要看具體需要給什么串口權(quán)限,虛擬串口一般叫做/dev/ttyACM0,所以可以寫(xiě)入KERNEL=="ttyACM[0-9]*",MODE="0666" ,而真實(shí)串口一般叫/dev/ttyUSB0,可以用KERNEL=="ttyUSB[0-9]*",MODE="0666" 。)
3)STM32CubeIDE報(bào)錯(cuò)GDB服務(wù)端無(wú)法打開(kāi)。
我在博客里已經(jīng)給出了詳盡的解釋
關(guān)于STM32CubeIDE無(wú)法正常啟動(dòng)GDB服務(wù)端的解決辦法 - 墨髯 - 博客園 (cnblogs.com)
五、備注
1、實(shí)際上,很多的配置都需要看自己的需求來(lái)搞,我之前就盲目抄了其他人的時(shí)鐘樹(shù)配置,導(dǎo)致設(shè)備無(wú)法被電腦識(shí)別。所以如果出現(xiàn)問(wèn)題,最好先去翻翻官方文檔。很多問(wèn)題都可以通過(guò)官方文檔來(lái)解決。
2、整片文章里,我?guī)缀鯖](méi)有提到過(guò)函數(shù)報(bào)錯(cuò)的問(wèn)題,主要是我暫時(shí)沒(méi)有考慮關(guān)于報(bào)錯(cuò)的問(wèn)題,所以代碼中很少會(huì)有關(guān)于報(bào)錯(cuò)的內(nèi)容。這個(gè)問(wèn)題,可以等以后完善此通訊協(xié)議時(shí)解決。
審核編輯:湯梓紅
-
通信協(xié)議
+關(guān)注
關(guān)注
28文章
911瀏覽量
40403 -
Linux
+關(guān)注
關(guān)注
87文章
11342瀏覽量
210222 -
STM32
+關(guān)注
關(guān)注
2271文章
10923瀏覽量
357215 -
串口通信
+關(guān)注
關(guān)注
34文章
1627瀏覽量
55675 -
Ubuntu
+關(guān)注
關(guān)注
5文章
566瀏覽量
29966
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論