整合自公众号「HeartLikeCalifornia」 一、嵌入式编程基础一个嵌入式工程师的自我修养,大补习系列。 Part 1: 为什么我们要用RTOS。 这是在career fair被问到的问题,结果竟然想不起来了。瞬间觉得自己还是回去回炉重造吧。RTOS与传统的super loop sequential executing最大的区别就在于实时性。对于实时性要求非常高的时候,如果是传统的一个大循环,那等执行到你time critical的代码的时候就已经不知道什么时候了。所以为了把实时性永远放在优先级最高的位置上,就有了RTOS:将不同的任务变成一个一个独立的task,系统根据需要启动不同的task,这样总是能保证如果一个time stamp到达了,该time stamp对应的task能够立刻得到执行。 Part 2:Memory and Compile. ICS大补习。 嵌入式系统的存储空间非常有限,为了充分利用每一块空间,对于memory map自然要一清二楚。主要的存储类型是Flash和RAM:
一个典型的embedded system memory map如下: RTOS有关的核心部分存储在Flash最低地址处。.text部分是代码,.rodata部分是只读数据,例如带有const关键字的变量,这里还包括print中输出的字符串常量。.data是有初始化值的变量,.bss是没有初始值的变量,初始化值存储在Flash里,所以程序开始运行的第一步就是把初始值从Flash load到RAM里。 很多ARM core, RTOS operating system会把code, data copy到RAM中,增加速度。这里比较玄幻的一点就是不同芯片会采取不同的方法,具体还是要看技术手册或者Documentation。 例如我现在在用的Photon板子,采用STM32F205RGY6120Mhz ARM Cortex M3,运行FreeRTOS,Flash有1M,RAM有128K,但是按照技术手册,user可以用的Flash是80k,RAM有60k(可见Free RTOS所占的空间还是很大的,上面我画的图的比例有问题……),官方给出的memory map如下。[3][4] stack空间也是有限制的,在main loop thread中不能超过6144byte,在software timer thread中不能超过1024byte. 编译时会有如下输出: 可以看出,Flash的占用就是text+data(初始值),RAM的占用就是data+bss。这里Flash和RAM的总空间有误。Flash应该大约在80k,RAM应该大约在60k。 Part3: 关键字总结 Const: Read only, in flash. Static: private or share by the same function。这里注意的是,static关键词一定会被存储在.data或者.bss中,不会被存储在栈里,保证有唯一的访问地点。in RAM. global: in RAM const char * v.s. const char * const: 前者是不能改变字符串内容,但是可以改变指针。后者不能改变字符串内容,也不能改变指针。const在谁前面谁就是不能修改的。 Part4: 数据类型 Embedded System中数据类型与普通的PC中不一样。不明白的时候就用uint8_t, uint16_t, uint32_t吧。 char – 8bit short – 16bit / 又名shortint long -32bit / 又名longint float – 32bit double - 64bit string – 16bytes on heap initially, max is622 bytes [3] Photon FAQ | Code Size Tips. https://docs.particle.io/faq/particle-devices/code-size-tips/photon/ [4] Photon Datasheet. https://docs.particle.io/datasheets/photon-(wifi)/photon-datasheet/ 二、MQTT之前没有听说过,专门为IoT相关应用设计的轻量型、低能耗传输协议,适用于传输带宽受限的移动端。核心部分是publish/subscribe模型(p/s model)(如下图),非常好理解:类似杂志订阅,可以订阅某个“杂志”,同时有节点分发该“杂志”,订阅节点和分发源头节点都讲数据发送给中心server/broker,由中心进行转发。这样的协议有着根本性的好处——多个传感器多个接收平台情况的处理将变得非常简单。试想,当有多个传感器在publish数据,多个接收平台subscribe了其中一部分传感器的数据,如果不用p/s进行处理的话,每条连接都要手动确认连接,将会非常的麻烦。所以这个协议的核心是将端到端的信息传输分为了两段。不愧是为IoT相关应用设计的轻量型协议。 图一 MQTT的publish/subscribe模型 此图是我对MQTT的直观理解。假设MQTTclient 1是一个二氧化碳浓度传感器,它以特定的频率实时播报(publish)空气质量的情况,它把自己播报的内容起名为air_quality_1。当它把自己的数据发送给server或者叫broker的中心点时,该中心点会检查自己的subscribe list,找到订阅(subscribe)了air_quality_1这个信息的是MQTT client 2,那么它就会直接把数据转发(forward)给MQTT client 2。 以python的paho.mqtt库中的publish single函数为例,这里比较重要的几个因素如下: 注:这里的single指的是Publisha single message to a broker, then disconnect cleanly. 与之相对的是multiple,同时发送多条信息,每条信息的组成是msg ={‘topic’:”<topic>”, ‘payload’:”<payload>”, ‘qos’:<qos>,‘retain’:<retain>}。此部分内容参考[1]。 - topic 正如前面所描述的,有点类似CDMA技术,这里区分不同消息靠的就是topic,有点类似杂志订阅的栏目,是个字符串。你订阅了这个topic,我才会给你发。 - payload 即消息内容,例如字符串。 - qos 整个协议里面最难懂的概念!qos分为三档:[2]
随着信道不稳定性增强,提高QoS的级别可以更好地保证信息的成功传递。如之前图一中讲的那样,一次端到端的信息传输实际上包含两段通路publish和forward,如果这两段通路的QoS设定不一样怎么办?对这个疑问我手动进行了测试。发现:publish的时候直接按publish选定的QoS发送,forward的时候选择pub和sub设定的QoS的最小值。 具体而言,如果publish选择QoS1,subscribe选择QoS2,那么publish自然是QoS1,forward也选择QoS1;如果publish选择QoS1,subscribe选择QoS0,那么publish自然是QoS1,forward则会按照QoS0进行。即:传输质量可以降级,但是不可以升级。就像坐高铁买了二等座,你可以站着,也可以坐二等座,但是你不能去坐一等座。 - retain是处理一种特殊情况的flag。比如两个client,client 1先连接上server并publish了air_quality_1,之后client 2才连接上server并subscribe了air_quality_1。按照常理,也就是retain是False时,client 2不会收到client1刚刚发送的消息。但是如果retain时True,server上会保存上次发送的消息,在client 2 subscribe时直接转发过去。类比的话就是,订阅的杂志为了让你对于你订的是个什么东西有个大致的概念,在你订阅的时候会自动附赠你上一期的该系列杂志。 - hostname和port server/broker的hostname/ip,以及MQTT的端口,一般是1833。 - clientid 对于连接没有本质作用,用来区分不同用户。这样你在server的log里就知道时谁连接上了,发送了什么,易于debug? - keepalive 有点类似Wi-Fi连接中一段时间没有消息就会发送beacon确认连接是否还存在,python这里默认60s静置过后client会发送beacon向server确认连接是否还在。 - will 并没有用到 - auth auth = {‘username’:”<username>”,‘password’:”<password>”}。用户名和密码,确保数据发送的安全性,至少你需要知道用户名和密码才可以“窃听频道”。 - tls 并没有用到 - protocol 选MQTTV311 - transport set to “websockets” to send MQTT over WebSockets. Leave atthe default of “tcp” to use raw TCP。这里补充说明一下,MQTT只是负责数据分发的协议,数据通信上本质还是用的TCP或者UDP。websocket和TCP的区别:TCP是流式发送,收到的信息是一个数据流,可能会由于网络中的多种情况导致接收端接收到的不足值或者多余值。而websocket在TCP的基础上把数据打包成一个整体,整体到达接收端后触发event handler。 [1]paho-mqtt 1.4.0 Documentation. https://pypi.org/project/paho-mqtt/#single [2]MQTT Essentials Part 6: Quality ofService. https://www.hivemq.com/blog/mqtt-essentials-part-6-mqtt-quality-of-service-levels 自己在debug MQTT的时候,发现打开client和server所有端点上的log更容易弄清楚正在发生什么事。起初发现发送消息接收不到的时候一头雾水,后来渐渐明白是在QoS2下,server返回给publish节点的PUBREC没有得到回应,server就一直在发PUBREC,publish节点始终没有发送PUBACK。最终发现是photon github库中根本就没有PUBREC接收并再次发送PUBACK这一语句……由此达成了本人第一次github pull request成就。 无论是python的MQTT库还是photon的MQTT库,这里对于后续过程的处理都在loop函数中完成。即第一次publish完后,后续的发送和接收工作都在loop函数中,e.g.如果收到了PUBREC就发送PUBACK。这make sense: 因为你也不知道返回的信息什么时候到,与其block在一个函数中,不如进入一个loop。但是这样也不合理,进入loop就进入了死循环,如果应用要求视情况不同发送数量不等的后续数据,就会很难办。这里还是需要理解根本协议,然后在别人写好库的基础上作出自己需要的修改。 三、Serial前两天解决的一个问题是,想给SD卡写数据,怎么写。选项一是通过Serial把数据通过UART串口发送到板子上,板子再写入SD卡。选项二是通过TCP网络连接把数据发送到板子上,板子在写入SD卡。前者只建立了一个连接,所以如果想要debug,需要板子echo接收到的字符。后一种方法有两个连接,分别是数据源头PC机和单片机(TCP),单片机和它的串口连接的机器(UART Serial),可以在TCP连接发送数据的同时,通过串口输出进行检查。因此前一种方法速度快连接稳定,但是很难知道里面正在发生什么,容易有噪音,而后一种方法容易掉线(实验中TCP连接断开后很难再连上,且TCP header很多,发送较慢),但是发送数据比较稳定,自动排除噪音。 考虑再三还是选择了串口,我调就能让它变得更稳定,但是TCP连接断开就实在无法控制它连上了……(实验中发现断开的可能性与发送数据速率有关,发送越快,越容易断开。也许通过定时发送beacon来避免断开,个人猜测……) 所以这里来补习一下串口的知识。 串口大致分为两种:带缓冲区的和不带缓冲区的。 1. 所谓带缓冲区,就是收到的东西会先存入一个buffer,应用读取数据的操作全都是与buffer进行交互。例如arduino中Serial.available()是返回buffer中待读字符的数目,Serial.read()是从buffer中读取一个字符,如果buffer已满,则等待应用读取走字符,这期间新到达的字符不会存入buffer。buffer的具体实现采用的是循环队列,这点可以在github源码上找到: 了解到这一点之后,就会知道,Serial.available()是一个实时性的函数,如果当前没有buffer,则会立刻返回。所以这里如果没有了解清楚的话就会写出下面这种典型错误代码: 中间的可以省去不看。重点是,如果把Serial.available放在循环的判断条件中,万一有某个瞬间字符还没到缓冲区里,那么整个读取的循环就退出了……显然是错的。 正确的语法应该是: while(1) { if(Serial.available()) { Serial.read(); } } 注意,linux中说无法打开串口很可能是权限问题,需要sudo。一般插上USB就可以打开串口。 另外一个stream-based的典型例子是printf。当你printf的时候,并不是直接输出到terminal,而是输出到一个buffer,当buffer累积到一定程度的时候,才会输出到terminal。所以如果想要看实时的结果,需要fflush()或者printf(“\r\n”)。我自己测试的时候遇到了一个很玄的问题:如下图,printf后紧接fflush()和printf(“\r\n”)两种情况下输出的不一样,后者输出的数据有更多的乱码,所以猜测这里即便是fflush()也没有做到实时输出。 2. read()和write()这两个傻瓜函数就是不带缓冲区的。read就是实时监测当前是否有读到字符,所以可能会返回0(没有读到任何字符),也可能返回不足值(间隔太久,没有一次读完)。所以调用read的时候一定要小心地考虑各种情况,比如上面这张图里我就不得已使用了while循环阻塞。 上面说了这么多,其实还没有讲一些最基本的UART串口协议。UART串口协议是最傻瓜、最直观、最慢、最不稳定的交流协议。一个packet非常的简单,从高电平拉到低电平为start,从低电平返回高电平为finish,中间一般是8N1,8个bit数据,没有校验位,一个终止符。所以问题就很显然——不稳定。尤其是在没有发送数据的时候,电平随便变化一下,接收端就以为自己收到了什么东西,这就是为什么UART串口协议中有非常多的乱码,也是为什么我们有必要加一重判断。但一般情况,正在发送数据的话,还是比较稳定的。 说它慢也是真的,最快的串口传输速度是115200bit/s,换算一下大约14kB/s,稍微有点概念就知道这有多慢了…… 注意UART串口通信协议不是USBSerial Protocol,一开始我在网上查USB Serial Protocol,找到了非常高级非常复杂的协议,但是总觉得跟我想象的不一样,然后才发现这是完全不同的两码事……猜测USB Serial Protocol是指U盘这些东西传输数据的协议。 四、Raspberry Pi Raspberry Pi是嵌入式开发中低门槛、方便快速开发和测试的平台。其计算资源相比一般的单片机可谓是十分豪华,从1+GHz主频,512M/1G RAM,HDMI显示屏,videocore GPU,Wi-Fi和Bluetooth连接到Linux操作系统、可以跑python,这一切都让这个平台在开发中极易上手。这一切仅需要miniUSB的5V电压支持。但是它的资源配置显然是无法跟PC机以及服务器相比。所以综合而言,现在Raspberry Pi是IoT架构的中层,或者叫gateway的设备的不二之选。 Raspberry Pi家族成员众多,各有优劣,从最轻量级的zero到包含以太网口和4个USB Port的3B,可以根据应用选择不同的款式。 但是操作Raspberry Pi有一点很烦人……与其说像单片机,它更像一个PC机——一般而言,让一个Raspberry Pi运转起来需要如下配备:(还少了一个microSD卡读卡器) 虽然作为一个IT人士,谁没有多几个键盘鼠标HDMI连接线,但是把这些都装好,总有一种“摊开架势”的感觉,无论装还是收都得折腾半天。(想到自己大四的时候折腾它每次都跑到我妈办公室“偷”鼠标和键盘……)所以这个推送的重点就是讲一讲,如何能够“headlessly boot”,也就是,仅需要Raspberry Pi,电源线,SD卡和读卡器。这里主要针对最轻量的Zero系列,因为它接显示器、鼠标和键盘都只能通过一个miniUSB,组装难度大大增加(主要是懒),所以一般用Zero时都是采用headless的方式。 Step 1. 下载Raspbian StretchLite. Raspbian操作系统是开源的,从RaspberryPi可免费下载。下载时有NOOBS和Raspbian两种选择,官网上说NOOBS适用于没有Linux操作经验的人士。但这两个的区别似乎也只是,下载NOOBS的话只需要把整个解压后的文件copy到SD卡中,而Raspbian需要烧录。这里我们要对boot文件进行操作,所以选择Raspbian;考虑到zero的轻量级,所以选择Lite版本的Raspbian。 Step 2. 下载烧录工具。 网上推荐比较多的是Win32 Disk Imager(适用于Windows)和ETCHER(专为IoT开发,有Windows, Mac和Linux版本)。哪个都很好用,选择待烧录文件、烧录位置,一键烧录! Step 3. 将SD卡插入读卡器,读卡器插入PC机。 用下载好的烧录工具把磁盘映像(.img)烧录到SD卡中。这里的话,如果你跟我一样用的是有两个或多个卡槽的SD读卡器(如下图),插入Windows操作系统后,没有插入SD卡的那个卡槽也会显示出来(例如,F盘),同时系统弹出说这个盘使用前需要格式化,但是如果你点格式化肯定是会失败的因为这个SD卡压根就不存在=_=插入SD卡的卡槽则会正常读取显示,例如,E盘。同时提醒,使用SD卡读卡器插入和弹出要谨慎一些,之前借师兄的读卡器,师兄的电脑是Mac,我的是Windows,反正一插上就是要我格式化,但是格式化从来都是失败。大概就是读取失败了,不知道是什么时候坏的…… Step 4. 修改配置文件,启用ssh和配置Wi-Fi 如果你使用的是Mac或者Linux系统,可以直接把烧录后的盘mount到电脑上,这样可以直接修改操作系统的/(root)部分,参考[3]。但是我的Windows系统下只能看到/boot的部分,其他部分都是隐藏的。所以这里采用方便的直接在/boot部分增加两个文件来完成配置。 首先是直接在根路径(一进入SD卡)下新建名为ssh文件,无后缀,不需要在文件中写任何东西。之后是新建wpa_supplicant.conf的文件,这个文件就是Wi-Fi配置文件,boot时会自动被copy到/etc/wpa_supplicant /wpa_supplicant.conf。在此文件中编辑如下内容: country=US ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev update_config=1 network={ ssid="WIFI_SSID" scan_ssid=1 psk="WIFI_PASSWORD" key_mgmt=WPA-PSK } WIFI_SSID和WIFI_PASSWORD处填写自己的Wi-Fi名和密码。这里注意: (1) 只有WPA-PSK类型的Wi-Fi可以使用,也就是使用事先设置好的密码的。如果是学校里的那种Enterprise网络是不行的,大约太过复杂…… (2) 整段都要copy。有的网页或者论坛上说只需要copy后半部分network={}就可以,但我的测试中从来都没有连接上……欢迎指点! (3) Wi-Fi的选择。之后想要连接到Raspberry Pi需要知道它的IP地址。如果选择家里的路由器,需要再额外下载安装IP扫描软件,例如Angry IP Scanner。这里像我这种比较懒的人就会直接选择开电脑的移动热点,这样连接上的device的IP一目了然。虽然也有论坛说ssh连接的时候,如果网络里只有一个Raspberry Pi可以不需要它的IP,直接ssh pi@raspberrypi.local,但我自己的测试中从来没有成功过。上网查查得知有可能是因为家里的IP路由器不提供domain name service……总之是跟网络配置有关,不想在这方面卡住的话还是直接开热点吧。 Step 5. Boot! 正常情况下,Raspberry Pi的绿灯会亮。如果是红灯亮了,肯定是出了什么错误。正常情况下,30-90s的时间内,Raspberry Pi就会Boot并连接到网络上。 之后就可以通过ssh pi@192.168.137.6连接了。可以通过ifconfig和iwconfig来查看网络连接是否正常。这里我还顺便查看了网络连接的一些关键文件,查看是否已经把wpa_supplicant.conf给copy过去。如果给Raspberry Pi断电后再次读取SD卡,会发现之前创建的ssh和wpa_supplicant.conf文件都不见了。 之前第一次尝试headlessly boot的时候华丽失败,师兄让我一个上我把一个简单的代码在Raspberry Pi Zero上跑起来,但是因为忙着开组会,11点才开始搞的我即便是放弃了午饭也没能在12点半前搞出来,直接死在boot上,根本没找到插上电的Pi的IP。放假前最后一天“不要脸”地又找师兄要了两个Zero回来尝试,哪里摔倒的就要在哪里爬起来嘛。然后很快地就发现问题在于写wpa_supplicant.conf的时候没有copy第一段,虽然我现在也不知道缺少那一段为什么就连不上了…… Performance Monitor on RPi 包括CPU frequency, CPU utilization, CPU temperature, Performance Counters。另外再附加一些实用的脚本,包括bandwidth setting, Bluetooth, Wi-Fi。所有的代码都整理好存储在Github上,点击阅读原文即可链接过去。 注意: (1) 所有脚本都在RPi 3B上测试过。有些指令适用于所有Linux系统,或者经过一些修改后便可在Linux系统上运行。 因为我组的主要研究跟power, performance相关,所以怎么能在RPi获取想要的performance数据就十分关键。而RPi跟其他运行RTOS的板子不同,不需要读底层代码和寄存器。因为RPi上运行的OS都是Linux-based的,而Linux的核心思想就是”一切都是文件”,所以这篇帖子的核心思想就是: 获取与performance相关的数据 -> 读取文件! 设定参数 -> 修改文件! 一切都可以通过command line来完成! 1. CPU Frequency 来源自UCSD CSE 237A课上project1中修改、读取frequency的操作。 显示各个core的frequency的指令如下: cat/sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq 会显示一串数字。如果想知道确切哪个core的frequency是多少,把*替代成corenumber即可。 设置频率的操作十分类似: # set userspace governor echo "userspace" >/sys/devices/system/cpu/cpufreq/policy0/scaling_governor # set frequency echo $freq >/sys/devices/system/cpu/cpufreq/policy0/scaling_setspeed 首先修改governor的设置。其默认设置是ondemand,也就是会根据workload自动调整frequency。所以首先把governor修改为userspace,之后就可以修改相应文件中的频率值了。RPi 3B中,可选的frequency只有600MHz和1200MHz。这里$freq的单位是kHz,所以$freq的取值为600000或者1200000。 Reset的操作就是把governor恢复为ondemand: echo "ondemand" >/sys/devices/system/cpu/cpufreq/policy0/scaling_governor 2. CPU Utilization CPU Utilization是CPU power的重要影响因素。下图是我在为RPi power建模时得到的一组实时结果,可以看到RPi整体的power很大程度上与CPU Utilization同步。这里的power prediction是通过frequency和utilization的线性组合得到的,可以达到平均大约3%的prediction error。 获取CPU status的指令为: cat /proc/stat 使用此指令,可以得到每个核在不同状态的cycle count。之后把其中在工作的cycle count加起来,除以总的cycle count,就可以得到utilization (%)了。代码稍微有点长,就不在这贴了。 注意一点是,在RPi 3B上,使用这种方法以200ms为间隔sample utilization本身就会产生大约30%的utilization。如果采样频率更高,自然带来更高的utilization overhead。这里就有点测不准原理的意思了:如果你想要采样,就一定会带来影响,那你测到的结果就不准了。 3. CPU Temperature 以下指令可以获得以millidegrees Celsius为单位的CPU Temperature: cat /sys/class/thermal/thermal_zone0/temp 这是Linux kernel自带的temperature interface。换句话说,Linux系统本身的thermal management就是基于这些指令。注意这里出现了一个概念叫做thermal zone。听起来很高大上的样子,但说白了就是那个temperature sensor的所在区域,所以它测得的数据只能大致代表这个区域的温度。如果要使用这些数据,需要搞清楚具体哪个thermal zone是哪些部位,这里就需要查看官方doc。 使用如下指令可以查看thermal zone和温度的对应关系: paste <(cat/sys/class/thermal/thermal_zone*/type) <(cat/sys/class/thermal/thermal_zone*/temp) | column -s $'\t' -t | sed's/\(.\)..$/.\1°C/' 除了Linux kernel自带的方法,还可以借助很多工具,例如vcgencmd, psensor。这些工具应该是更好用的,还可以有图形化界面,自动为你画出一些图线。如果只是监视CPU各种状态的大致水平,而不需要采样获取数据的话,这些工具是更适合的。 4. Performance Counters 所谓performance counters,就是类似instruction count, cache misses的参数。在UCSD CSE237A project1中,经过修改kernel,可以通过直接读取register中的数值得到performance counter的值。这些数据在CPU power prediction中同样好用,可以得到更低的error,只是有些应用中很难获取这些底层的状态数据。 不想hack那么底层的话,在Linux系统,perf stat这个工具就可以帮助你通过一行指令获取你想要的performance value: sudo perf stat -a -I 200 -e instructions,cache-misses,L1-dcache-loads,L1-dcache-stores,branch-instructions,branch-misses,cache-references,cpu-cycles,r110,r13C,r1A2,r1C2 sleep infinity 2>&1 -a指all cpu,即获取所有cpu core上的数据。-I 200意为以200ms的间隔采样。-e就是events,后面紧接的那一串就是设定的感兴趣的performance value。最后sleep infinity是一直进行的意思。2>&1是把stderr的输出合并到stdout中。这只是一个示例,更多操作可参考perf stat的官方doc。 5. Bandwidth Setting 想要通过命令行设置带宽,有一个非常好用的工具叫做wondershaper。例如我想要设置wlan0的上、下行带宽为100kbps,只要使用如下指令: wondershaper -a wlan0 -u 100 -d 100 这里-a是指定adapter,除了Wi-Fi,你也可以制定为eth0来设置有线网的带宽。-u和-d分别为设置uplink和downlink的带宽,这里的单位是kbps。 6.Bluetooth 在各种performance monitor之后,我们来聊聊Bluetooth on RPi。蓝牙作为一种低功耗的p2p传输,获得了人们的青睐。尤其在加入BLE之后,其功耗进一步降低,成为大多数low power chip传输数据的不二之选。如果真的有什么东西可以阻止蓝牙,那大概就是安全性问题了。具体不多说,这里的主题是:然鹅,想要在RPi上使用蓝牙,真的是费了好一番功夫。 首先,RPi上默认蓝牙是不开启的,想要开启它,你需要使用如下命令设置: sudo rfkill unblock bluetooth sudo hciconfig hci0 up 类似ifconfig,你可以通过hciconfig来查看其接口的UP or DOWN状态。 在使用上,你有以下几种选择:
在实验中,我选用了pybluez,RPi 3B在进行不同蓝牙操作时的power如下: 7. Wi-Fi 虽然我把Wi-Fi列在了这里,但我相信大家对于TCP, UDP的操作都很熟悉了。这里就展示一下实验得到的Wi-Fi power trace, 然后准备该吃吃该喝喝享受周末了~ -END- ---------------------------------------------------------------------------------------------------------------------- 我们尊重原创,也注重分享,文章来源于微信公众号:嵌入式ARM,建议关注公众号查看原文。如若侵权请联系qter@qter.org。 ---------------------------------------------------------------------------------------------------------------------- |