-
eMTC,全称是 LTE enhanced MTO,是基于LTE演进的物联网技术。为了更加适合物与物之间的通信,也为了更低的成本,对LTE协议进行了裁剪和优化。eMTC基于蜂窝网络进行部署,其用户设备通过支持1.4MHz的射频和基带带宽,可以直接接入现有的LTE网络。eMTC支持上下行最大1Mbps的峰值速率,可以支持丰富、创新的物联应用。eMTC的基本特性窄带LTE其中最主要的几个特性。第一,系统复杂性地大幅度降低,复杂程度及成本得到了极大的优化。第二,功耗极度降低,电池续航时间大幅度增强。第三,网络的覆盖能力大大加强。第四,网络覆盖的密度增强。eMTC具备LPWA基本的四大能力:一是广覆盖,在同样的频段下,eMTC比现有的网络增益15dB,极大地提升了LTE网络的深度覆盖能力;二是具备支撑海量连接的能力,eMTC一个扇区能够支持近10万个连接;三是更低功耗,eMTC终端模块的待机时间可长达10年;四是更低的模块成本,大规模的连接将会带来模组芯片成本的快速下降,eMTC芯片目标成本在1~2美金左右 。除此之外,eMTC还具有四大优势:一是速率高,eMTC支持上下行最大1Mbps的峰值速率,远远超过GPRS、 ZigBee等物联技术的速率,eMTC更高的速率可以支撑更丰富的物联应用,如低速视频、语音等;二是移动性,eMTC支持连接态的移动性,物联网用户可以无缝切换保障用户体验;三是可定位,基于TDD的eMTC可以利用基站侧的PRS测量,在无须新增GPS芯片的情况下就可进行位置定位,低成本的定位技术更有利于eMTC在物流跟踪、货物跟踪等场景的普及:四是支持语音,eMTC从LTE协议演进而来,可以支持 VOLTE语音,未来可被广泛应用到可穿戴设备中。原理与关键技术1、物理层资源结构eMTC作为LTE一个特性, 基本沿用LTE设计,占原有LTE系统的6个PRB,其中一个RB占12个子载波 (子载波带宽15kHz, 间隔为15kHz) 。时域结构上eMTC帧结构与LTE一致;频域结构上,3GPP将系统带宽划分成若干NB (不重叠的6个PRB) ,eMTC UE的调度受NB限制,不能跨NB调度,不同eMTC UE可以共享一个NB的资源。2、信号与信道eMTC不重用LTE的PDCCH、PCFICH和PHICH下行信道, 新增MPDCCH信道, 用于发送eMTC UE的PDSCH和PUSCH信道的调度指示以及公共消息的指示, 比如寻呼、RAR响应、上行ACK反馈。eMTC重用LTE的下行数据信道PDSCH, 支持传输模式为TM1/2/6/9;eMTC重用LTE的下行导频信号RS;重用LTE的物理同步信号PSS/SSS, 其中PSS映射到时隙0和时隙10的最后一个OFDM符号, SSS映射到时隙0和时隙10的倒数第二个OFDM符号,均以5ms为周期重复发送;eMTC重用LTE的物理广播信道PBCH, 新增一套SIB消息, 包括SIB1-BR、SIB2、SIB3、SIB4、SIB5和SIB14共6条,MIB消息新增一个IE用于携带SIB1-BR的调度信息, 在每个系统帧的0#子帧和9#子帧发送, 周期为40ms。eMTC的PRACH和LTE的PRACH分开 (使用相同频率, 时域上区分) , 可以采用时分, 频分, 码分方式;eMTC的PUCCH和LTE的PUCCH分开, eMTC的PUCCH支持跨子帧跳频, 不支持子帧内跳频;eMTC使用LTE传统的PUSCH信道上传数据资源, 其PUSCH资源受NB限制。3、资源共享与调度eMTC作为小区特性, 与LTE共小区部署, 不占用独立小区, 但是需要占用空口的RB资源和基带的处理资源, 为保证MBB业务优先, 系统会预留一定的资源给LTE, 即使LTE没有任何业务, eMTC也不能使用预留。通过配置参数EmtcDlRbTargetRatio和EmtcUlR bTargetRatio, 可以控制LTE和e MTC资源占用比例, 在LTE和eMTC负载均很高时, 依据两者目标利用率, 动态共享LTE的PRB资源, 如图3-case1;当eMTC负载较高, 而LTE有空闲RB资源时, 这些空闲RB资源可以给eMTC使用, 如图3-case2;因e MTC采用跨子帧调度和重复技术, 会长期占用RB资源, 为了避免LTE控制消息和VoIP等高优先级业务被长期阻塞, 通过DlLteRvsNbNum和UlLteRvsNbNum参数给LTE预留RB资源, 保证LTE业务的需求, 即LTE负载较而eMTC负载较低时, LTE可以占用全部带宽。4、峰值速率与LTE下行异步HARQ, 上行同步HARQ不同, eMTC上行下行都是异步HARQ。下行调度, 设MPDCCH重复的最后一个子帧编号n, 则MPDCCH调度的PDSCH起始子帧编号为n+2;设PDSCH重复的最后子帧编号为n, 则PUCCH 1 Ack/Nack子帧编号为n+4。上行调度, 设MPDCCH重复的最后一个子帧编号n, 则MPDCCH调度的PUSCH起始子帧编号为n+4;设PUSCH重复的最后子帧编号为n, 则MPDCCH Ack/Nack子帧编号为n+4。在无重复及重传的情况下,以ModeA对eMTC速率进行估算:下行调度周期为10ms, 全双工时可以下行连续MPDCCH和PDSCH调度, 10ms周期内能发送8个下行TB (传输块) , 每个TB最大1000bits,因此下行峰值速率为8*1000* (1000/10) =800kbps;同理下行半双工峰值速率为300kbps;上行行调度周期为8ms, 同理推算出全双工上行峰值速率1000kbps, 半双工上行峰值速率375kbps。5、功耗eMTC采用PSM和e DRX技术以节约功耗。PSM是一种新增的比Idle态更省电的省电模式, 由MME通过NAS配置给UE, UE发送完数据后在Idle态停留一段时间后进入深度睡眠态, 不监听任何空口消息, 只在主动发送数据和周期TAU时才退出PSM模式, 如图5 (a) ;eDRX通过延长Idle态或连接态的DRX周期, 减少UE侦听网络的信令处理, UE只在每个e DRX周期只在寻呼窗口内监听PDCCH, 其它时间处于睡眠状态, 从而达到UE节电的目的。应用场景eMTC是爱立信提出的无线物联网解决方案。eMTC基于LTE接入技术设计了无线物联网络的软特性,主要面向中低速率、低功耗、大连接、移动性强、具有定位需求的物联网应用场景。eMTC无线物联网技术可支持语音、移动、定位业务,适合进行速率为100kbit/s~1Mbit/s范围内的中速小包数据或语音业务,模组市场价约10美元每块,典型应用为智能电梯、行车、物流跟踪、穿戴设备等。
-
WiFi的历史从802.11的FHFS,DSSS到802.11b的DSSS,到802.11a的OFDM,802.11g的ERP(将OFDM从5G迁移到了2.4G),到802.11n的更宽频带(40MHz)的OFDM技术,到802.11ac的进一步拓宽(80, 80+80,160MHz)的OFDM技术,到802.11ax的更窄的子载波(78.125kHz)的OFDM技术,到讨论中的802.11be的320MHz的OFDM技术。OFDM技术从802.11a,大概2001年的时候,到现在,已经持续演进了20年了,它的抗多径效应,适合无线空间的复杂环境。因为802.11ax(Wi-FI 6)希望覆盖更广的空间,所以把载波宽度进一步变小了。每一代的发展,频带基本是越来越宽,似乎是可以无限的把频宽扩展下去。但是这应该是存在问题的,多样化的需求下,大频宽是可能浪费频段的。毕竟有些地方只需要小的频宽就好了。802.11ax还定义了一个仅支持20MHz的模式,也是瞄准了万物互联的趋势下,小数据,低能耗的搭配。但是802.11be(TB Wi-Fi 7)又把带宽变得更大了,每一代总是希望能更快的。但是单纯更快有什么意思呢。OFDM不止可以分频带,因为网络的特性,还可以分用户,这是Wi-Fi现在越来越看重的,希望越来越多的用户都是使用这个网络,而且还要能用。从802.11ac(2013)开始的Wi-Fi5已经引入了多用户的观念,对应的技术是MU-MIMO。它是这么样的,不同的用户用的是不同的天线。到了802.1ax(Wi-Fi6),支持上行的MU-MIMO,而且还把一个频段同时分配给了多个用户(OFDMA),每个用户最少可以只用2MHz的带宽。一下还把上行和下行都给加进去了。不止如此,还加入了BSS Coloring技术,弱化Exposed Node的问题,提升密集网络覆盖下的并行性。Wi-Fi6有这么多优势,其实它的主要着笔点在于密集用户,密集网络。想想现在Wi-Fi6的设备已经都出来1两年了,但是也并没有多改变生态。其实现在大家也觉得够用了,就像我自己,这种Wi-Fi6的理念对人这个用户来讲真的是有点超前了,哪里去找如此密集的人流和网络?办公场所,体育场?这些场合毕竟是少数。自动化生产车间也许是比较合适的,不过不是人流,而是物流。Wi-Fi7似乎要在这个道路上越走越远,越来越远…
-
单片机各种通信协议详解一、IIC通信协议(1)概述I2C(Inter-Integrated Circuit BUS) 集成电路总线,该总线由NXP(原PHILIPS)公司设计,多用于主控制器和从器件间的主从通信,在小数据量场合使用,传输距离短,任意时刻只能有一个主机等特性。经常IIC和SPI接口被认为指定是一种硬件设备,但其实这样的说法是不尽准确的,严格的说他们都是人们所定义的软硬结合体,分为物理层(四线结构)和协议层(主机,从机,时钟极性,时钟相位)。IIC,SPI的区别不仅在与物理层,IIC比SPI有着一套更为复杂的协议层定义。下面来分别说明一下IIC的物理层和协议层。(2) IIC的物理层A 只要求两条总线线路,一条是串行数据线SDA,一条是串行时钟线SCL。(IIC是半双工,而不是全双工)。每个连接到总线的器件都可以通过唯一的地址和其它器件通信,主机/从机角色和地址可配置,主机可以作为主机发送器和主机接收器。IIC是真正的多主机总线,(而这个SPI在每次通信前都需要把主机定死,而IIC可以在通讯过程中,改变主机),如果两个或更多的主机同时请求总线,可以通过冲突检测和仲裁防止总线数据被破坏。传输速率在标准模式下可以达到100kb/s,快速模式下可以达到400kb/s。连接到总线的IC数量只是受到总线的最大负载电容400pf限制。二、SPI 协议通常SPI通信要求4根线,分别是MOSI(mast output salve input), MISO, CLK, CS。当发送和接受数据的工作都准备好了,只要有时钟CLK,就发送数据,没有时钟就不发送,而且一个时钟周期发送一位(bit)数据,所以发送数据的快慢由时钟频率来控制。3.至于时钟和数据的相位没有特别严格的要求(而IIC中,数据的变化只能在SCL是低电平的时候发生),SPI数据的变化是一个时钟周期一次,这样的方法来传输数据就简单多了。我们可以根据需求对时钟的极性和相位做调整,看看是在时钟上升沿还是下降沿来发送数据,还有停止发送时时钟的极性,是保持高电平还是低电平。4.另外在多机通信时,SPI只是简单的通过一个片选信号来选择哪个设备占用总线,但是IIC是通过发送从设备地址来自动选择的。三、什么是TTL电平、CMOS电平?TL电平信号被利用的最多是因为通常数据表示采用二进制规定,+5V等价于逻辑"1",0V等价于逻辑"0",这被称做TTL(晶体管-晶体管逻辑电平)信号系统,这是计算机处理器控制的设备内部各部分之间通信的标准技术。CMOS电平和TTL电平:CMOS电平电压范围在3~15V,比如:当5V供电时,输出在4.6以上为高电平,输出在0.05V以下为低电平。输入在3.5V以上为高电平,输入在1.5V以下为低电平。而对于TTL芯片,供电范围在0~5V,常见都是5V,如74系列5V供电,输出在2.7V以上为高电平,输出在0.5V以下为低电平,输入在2V以上为高电平,在0.8V以下为低电平。因此,CMOS电路与TTL电路就有一个电平转换的问题,使两者电平域值能匹配TTL高电平3.6~5V,低电平0V~2.4VCMOS电平Vcc可达到12V四、RS-232协议RS232(异步传输标准接口),是个人计算机上的通讯接口之一,也称串口或串行通讯接口。由电子工业协会(Electronic Industries Association,EIA) 所制定的异步传输标准接口。通常 RS-232 接口以9个引脚 (DB-9) 或是25个引脚 (DB-25) 的型态出现,一般个人计算机上会有两组 RS-232 接口,分别称为 COM1 和 COM2。是目前最常用的一种串行通讯接口。标准RS232接口: 常用串口只需要TX与RX即可。USB转RS232串口线五、CAN总线CAN是控制器局域网络(Controller Area Network, CAN)的简称,是由以研发和生产汽车电子产品著称的德国BOSCH公司开发的,并最终成为国际标准(ISO 11898),是国际上应用最广泛的现场总线之一。 在北美和西欧,CAN总线协议已经成为汽车计算机控制系统和嵌入式工业控制局域网的标准总线,并且拥有以CAN为底层协议专为大型货车和重工机械车辆设计的J1939协议。CAN 是Controller Area Network 的缩写(以下称为CAN),是ISO国际标准化的串行通信协议。在汽车产业中,出于对安全性、舒适性、方便性、低公害、低成本的要求,各种各样的电子控制系统被开发了出来。由于这些系统之间通信所用的数据类型及对可靠性的要求不尽相同,由多条总线构成的情况很多,线束的数量也随之增加。为适应“减少线束的数量”、“通过多个LAN,进行大量数据的高速通信”的需要,1986 年德国电气商博世公司开发出面向汽车的CAN 通信协议。此后,CAN 通过ISO11898 及ISO11519 进行了标准化,在欧洲已是汽车网络的标准协议。CAN 的高性能和可靠性已被认同,并被广泛地应用于工业自动化、船舶、医疗设备、工业设备等方面。现场总线是当今自动化领域技术发展的热点之一,被誉为自动化领域的计算机局域网。它的出现为分布式控制系统实现各节点之间实时、可靠的数据通信提供了强有力的技术支持。CAN的报文格式在总线中传送的报文,每帧由7部分组成。CAN协议支持两种报文格式,其唯一的不同是标识符(ID)长度不同,标准格式为11位,扩展格式为29位。在标准格式中,报文的起始位称为帧起始(SOF),然后是由11位标识符和远程发送请求位 (RTR)组成的仲裁场。RTR位标明是数据帧还是请求帧,在请求帧中没有数据字节。控制场包括标识符扩展位(IDE),指出是标准格式还是扩展格式。它还包括一个保留位 (ro),为将来扩展使用。它的最后四个位用来指明数据场中数据的长度(DLC)。数据场范围为0~8个字节,其后有一个检测数据错误的循环冗余检查(CRC)。应答场(ACK)包括应答位和应答分隔符。发送站发送的这两位均为隐性电平(逻辑1),这时正确接收报文的接收站发送主控电平(逻辑0)覆盖它。用这种方法,发送站可以保证网络中至少有一个站能正确接收到报文。报文的尾部由帧结束标出。在相邻的两条报文间有一很短的间隔位,如果这时没有站进行总线存取,总线将处于空闲状态六、485总线在要求通信距 离为几十米到上千米时,广泛采用RS-485串行总线标准。RS-485采用平衡发送和差分接收,因此具有抑制共模干扰的能力。RS232串口可以与485之间互转,在单片机上使用485总线与使用RS232串口一样,需要使用芯片转换电平即可!七、Modbus通讯协议Modbus协议是应用于电子控制器上的一种通用语言。通过此协议,控制器相互之间、控制器经由网络(例如以太网)和其它设备之间可以通信。它已经成为一种通用工业标准。有了它,不同厂商生产的控制设备可以连成工业网络,进行集中监控。此协议定义了一个控制器能认识使用的消息结构,而不管它们是经过何种网络进行通信的。它描述了一个控制器请求访问其它设备的过程,如何回应来自其它设备的请求,以及怎样侦测错误并记录。它制定了消息域格局和内容的公共格式。当在同一Modbus网络上通信时,此协议决定了每个控制器需要知道它们的设备地址,识别按地址发来的消息,决定要产生何种行动。如果需要回应,控制器将生成反馈信息并用Modbus协议发出。在其它网络上,包含了Modbus协议的消息转换为在此网络上使用的帧或包结构。这种转换也扩展了根据具体的网络解决节地址、路由路径及错误检测的方法。此协议支持传统的RS-232、RS-422、RS-485和以太网设备。许多工业设备,包括PLC,DCS,智能仪表等都在使用Modbus协议作为他们之间的通信标准。Modbus具有以下几个特点:(1)标准、开放,用户可以免费、放心地使用Modbus协议,不需要交纳许可证费,也不会侵犯知识产权。目前,支持Modbus的厂家超过400家,支持Modbus的产品超过600种。(2)Modbus可以支持多种电气接口,如RS-232、RS-485等,还可以在各种介质上传送,如双绞线、光纤、无线等。(3)Modbus的帧格式简单、紧凑,通俗易懂。用户使用容易,厂商开发简单。
-
IOT低功耗设备设计大致为3个方面的设计:器件选型、电路设计、软件设计、续航寿命估算——器件选型典型的器件包括:单片机MCU、电源芯片、通讯模组等。单片机——1.选择具备多种低功耗工作模式的MCU,如国民技术N32G4FR系列MCU支持5种低功耗模式(Sleep,Stop0,Stop2,Standby,VBat),开启带有RTC唤醒的Stop模式可让功耗尽可能低;2.支持宽范围供电,如1.8-3.3V,在不需要大电流供电的模式下,使用1.8V供电可以让MCU处于更低功耗的状态;3.不使用的IO配置为模拟输入,模拟输入模式下漏电流最低;关于MCU的超低功耗设计,参考该篇文字《STM32芯片超低功耗设计思路》电源芯片——1.选择更高效率的电源IC,开关电源DC-DC的效率高于LDO,特别在高压差、大电流的情况下,DC-DC具备更高的能效优势,对于常供电的IC,关注静态电流值,对于带EN管脚的IC,关注Shutdown电流值;2.LDO的成本比DC-DC低,且在低压差、低电流的情况下,具备低功耗特性的LDO也可做考虑,如圣邦微的SGM2034,静态漏电流为1uA;关于LDO与DC-DC的效能优势对比,参考该篇文字《LDO与DC-DC 的入门理解》通讯模组——1.通讯模组中的MCU部分可参考单片机的的低功耗设计,本质上具备一致性;2.2.4G的通讯模组,ZigBee低功耗具备更大优势,BLE蓝牙Mesh这两年间也开始逼近ZigBee,WiFi则比较大,同等条件下,ZigBee的发射电流可以做到50mA以内,而WiFi的发射电流一般要大于300mA,加上心跳包对接时间的差异,具备快联特性的WiFi可能需要10ms,而ZigBee可能只需要3-5ms。3.通讯模组OTA的功耗 > 搜网功耗 > 静态功耗。另外,网关信号正常与异常,也会导致通讯模组在搜网时的功耗有所不同。电路设计1. 对于耗电比较大的器件,使用独立IC供电,并尽可能做到可独立关断供电回路,在非常供电的状态下切断供电回路;2. 对于上下拉电阻,在确保信号抗干扰度良好的前提下,尽可能使用高阻值;如对于1K的上拉电阻,当电流回路对地时,产生3300uA的电流,而对于100K的上拉电阻,则为33uA。当然,对于外界的工频干扰等,同样的条件下,10K的上拉电阻具备更高的抗干扰度;3.电池电量检测采用分压电阻时可使用1M左右的阻值,由于涉及单片机ADC阻抗匹配的不同(关于ADC阻抗匹配,可参考《单片机读取外部电压ADC阻抗匹配问题》),建议在信号的采集中间加上一级电压跟随器,该跟随器需要低功耗或者需要单独供电,避免无谓的电量损耗;4.对于有光显示的场景,如LED指示等,尽可能降低LED亮度。软件设计软件设计更多地体现在如何驱动硬件进入低功耗模式,如:开启单片机RTC唤醒的Stop模式;控制电源的EN管脚进入非常供电模式;GPIO的模拟输入模式;通讯模组在发送完成数据之后,立即关闭UDP连接,尽可能降低大电流模式持续时间续航寿命估算1.对于静态电流,可使用万用表进行测量(如Fluke的17B+),由于万用表的采样率较低,且所呈现的数值为测量有效值,因此对于动态电流,如设备的间隔性心跳包电流,则需要使用采样率更高的仪器进行测量,如Keysight的N6705C;(关于低功耗测量仪器,可参考《浅谈4款低功耗电流测试“神器”》)2.严谨的功耗计算中,需考虑电池的自放电率,即电池即使在不使用的条件下,自身的电化学物质也会产生一定的反应自我消耗,特别是可充电的镍镉电池;3.简单举一个低功耗设备续航时间计算的例子:假设电池容量250mAh,10分钟发送一次心跳包对接网络,每次5秒30mA瞬时电流,待机20uA电流,可做如下推算:单次对接网络耗电:30mA x 5s = 150mAs = 41.66uAh;一天对接网络次数:(24h x 60)÷10 = 144次;一天对接网络总时间:5s x 144 = 720s;一天待机总时间:(24h x 3600)s - 720s = 85680s = 23.8h;一天总功耗:(23.8h x 20uA) + (144 x 41.66uAh) = 6475.04uAh = 6.48mAh;可使用天数:250mAh ÷ 6.48mAh ≈ 39天原创不易,如果我的文字对你有所启发或帮助,还请“点赞、收藏、转发”三连!
-
Googletest Primer1. Googletest简介1.1 特点目标语言:C++平台:Linux/Windows/Mac2.2 优势1. 测试独立可重复。允许单独跑某一个测试用例,方便debug。2. 组织清晰。相关联的测试用例可以组织在一起,共享数据。3. 平台中立。可用于不同的OS、编译器。4. 提供错误的更多信息。在不出现“致命”错误的情况下继续跑下一个测试用例。2. Googletest命名法 Test/Test Case/Test Suite,测试/测试用例/测试套,傻傻分不清楚。 常规理解的测试用例(Test Case)在googletest中叫测试(Test),测试套(Test Suite)叫测试用例(Test Case)。3. Assertion断言3.1 基本概念断言用来检查某个条件是否成立,是googletest的基础。一个断言的结果有3种状态:success,nonfatal failure和fatal failure。断言结果为fatal failure时,程序立即终止;其他情况程序继续运行。一个Test通过断言来验证代码的行为是否如预期。如果一个Test崩溃或者有失败的断言,则此Test失败。一个Test Case包含一个或多个Test。Test应该被组织成多个Test Case以反映被测代码的结构。当一个Test Case中的多个Test需要共享数据或者例行程序时,可以把他们组织成一个test fixture class。一个测试程序可以包含多个Test Case。3.2 Assertion详解Googletest中的assertion实际上就是宏。当一个断言失败时,googletest除了会打印出失败信息外,还会打印出失败断言的源文件以及代码行数定位。此外,用户还可以定制失败信息,定制的失败信息将追加在googletest自带的失败信息后。Googletest中有两类断言:ASSERT_*和EXPECT_*。ASSERT_*将在失败时生成fatal failure,并立即终止测试程序;而EXPECT_*生成nonfatal failure,当前的测试程序并不会终止。推荐使用EXPECT_*,因为它允许在一次测试中报告多个失败断言;但是当一个断言失败,其后续的断言的运行无意义时,则应该使用ASSERT_*。由于ASSERT_*会立即终止测试程序,后续的清理程序可能会被跳过,可能会造成内存泄漏。所以当你除了断言错误还得到了heap check error时,记得有内存泄漏的风险并自己评估是否有修复的必要。可以在断言后使用一个或多个 << 操作符打印定制错误信息,示例如下:3.1.%3 基础断言以下断言做最基本的true/false条件判断。Fatal assertionNonfatal assertionVerifiesASSERT_TRUE(condition);EXPECT_TRUE(condition);condition is trueASSERT_FALSE(condition);EXPECT_FALSE(condition);condition is false3.2.%3 对比断言这类断言比较两个值之间的大小Fatal assertionNonfatal assertionVerifiesASSERT_EQ(val1, val2);EXPECT_EQ(val1, val2);val1 == val2ASSERT_NE(val1, val2);EXPECT_NE(val1, val2);val1 != val2ASSERT_LT(val1, val2);EXPECT_LT(val1, val2);val1 < val2ASSERT_LE(val1, val2);EXPECT_LE(val1, val2);val1 <= val2ASSERT_GT(val1, val2);EXPECT_GT(val1, val2);val1 > val2ASSERT_GE(val1, val2);EXPECT_GE(val1, val2);val1 >= val2 对比断言中的值必须是可比较的,否则会得到编译错误。 如果用户自定义类型定义了比较操作符的含义,对比断言也适用。由于Google C++ Style Guide不鼓励这种做法,可以使用ASSERT_TRUE()或者EXPECT_TRUE()来判断两个用户自定义类型的值大小。 使用时,ASSERT_EQ(actual, expected)的优先级高于ASSERT_TRUE(actual == expected),因为对比断言失败时会打印出实际值和期望值。 ASSERT_EQ()也可以用于比较两个指针。如果是比较两个字符串,ASSERT_EQ()比较二者的内存地址是否相等。如果想比较两个字符串的内容是否相等,不要使用ASSERT_EQ(),而可以用ASSERT_STREQ()。如果想判断一个字符串是否为空,使用ASSERT_STREQ(c_string, NULL)。如果支持c++11标准,还可以使用ASSERT_EQ(c_string, nullptr)。用 *_EQ(ptr, nullptr) 或 *_NE(ptr, nullptr) 进行指针比较,不要用 *_EQ(ptr, NULL) 和*_NE(ptr, NULL)。3.3.%3 字符串断言这类断言比较两个字符串的内容。Fatal assertionNonfatal assertionVerifiesASSERT_STREQ(str1, str2);EXPECT_STREQ(str1, str2);两个字符串内容相同ASSERT_STRNE(str1, str2);EXPECT_STRNE(str1, str2);两个字符串内容不同ASSERT_STRCASEEQ(str1, str2);EXPECT_STRCASEEQ(str1, str2);不考虑大小写,两个字符串内容相同ASSERT_STRCASENE(str1, str2);EXPECT_STRCASENE(str1, str2);不考虑大小写,两个字符串内容不同 在这类断言中的”CASE”是忽略大小写的含义。一个空指针和一个内容为空的字符串含义不同。4. 写一个简单的测试创建一个测试的步骤为:1. 用TEST()宏来定义和命名一个测试程序,定义的测试程序就是普通的无返回值C++程序。2. 使用googletest assertion来检查值。3. 测试的结果取决于断言。如果有断言失败(fatally或non-fatally),或者程序崩溃,测试失败。所有断言成功通过,则测试成功。 TEST()的参数由宽泛到具体。第一个参数是Test Case的名字,第二个参数是此Test Case内的Test名称。所有的名称必须是有效的C++标识符,并且不能包含下划线。一个Test的完整的名称由它所属的Test Case和它自己的名称共同决定。不同Test Case内的Test可以有相同的名称。 以一个简单函数举例如何写TEST。 函数: 相应的测试用例: Googletest根据测试用例来组织测试结果,因此逻辑相关的测试应该在同一个测试用例中。测试和测试用例的命名规则应该遵守Google C++编码风格。5. Test Fixtures Test fixture可以使多个测试共享数据和配置。 创建fixture的步骤:1. 创建一个类,继承自::testing::Test,并用protected关键字修饰类体。2. 在类中声明后续测试将共享的数据对象。3. 必要时,写一个默认的构造器或者SetUp()函数来准备每个测试需要的对象。4. 必要时,写一个析构器或者TearDown()函数来释放在SetUp()函数中分配的资源。5. 必要时,定义测试共享的subroutine。当使用fixture时,测试应写成TEST_F()而不是TEST()。TEST_F()才能获取到test fixture中的共享数据和subroutine。 TEST()中第一个参数是所属测试用例名,而TEST_F()中第一个参数时test fixture class的名称。所以在TEST_F()之前就要定义好要使用的test fixture class。 对每一个以TEST_F()方式定义的测试,googletest都会在运行时生成一个新的test fixture,并通过SetUp()函数来初始化、TearDown()函数来执行清理工作。即使是在同一个测试用例中的测试,它们在运行时都会有新的不同的test fixture对象,因此一个测试对fixture所作的修改不会影响到其他测试。 举例: FIFO队列类首先,定义一个fixture class。Fixture class的命名通常为FooTest,Foo为要进行测试的类。 编写的测试如下:在此例中,既使用了ASSERT_*,也使用了EXPECT_*。如果你想要测试程序继续下去以提供关于代码的更多测试信息,就使用EXPECT_*,但是当测试程序继续运行已经没有意义时,还是要使用ASSERT_*。当这些测试运行时,会发生以下事件:1. Gooletest创建一个QueueTest对象(这里简称为t1);2. t1.SetUp()初始化t1;3. 第一个测试(IsEmptyInitially)运行在t1上;4. 测试结束时t1.TearDown()进行清理工作;5. t1被销毁;6. 又一个t1被创建,第二个测试(DequeueWorks)运行于其上,上述步骤再重复一遍。6. 运行测试TEST()和TEST_F()隐含了向googletest注册的步骤。定义好测试后,可以通过RUN_ALL_TESTS() 运行。如果所有的测试均通过,RUN_ALL_TESTS() 返回0;反之,返回1。RUN_ALL_TESTS() 会运行你所连接的所有测试,即使是不同测试用例或者不同源文件中的测试。也可以通过gtest_filter和正则匹配运行指定的测试。
-
可参考的资料实在烧的可怜,这么重要的点,可参考资料竟然这么少。 大师们给个参考吧
-
小熊派LiteOS移植LVGL 一、移植前言 二、配置 TFT 三、LVGL 源码获取 四、显示接口移植 五、Demo 代码 六、实验现象 小熊派LiteOS移植LVGL一、移植前言之前使用小熊派实现了鸿蒙动画的开机界面,具体使用的技术栈为 STM32 + LiteOS + LVGL + FATFS +DMA 方式实现,刷新效率非常高,预览视频如下:关于这个的实现过程我会写一系列的教程分享出来,主要分为下面几个部分,本节为第二部分,基于 LiteOS 移植 LVGL 显示接口• 小熊派移植华为 LiteOS-M(基于MDK):链接 ;• 小熊派基于 LiteOS 移植 LVGL 显示接口:链接 ;• 小熊派基于 LiteOS 移植 LVGL 文件系统:链接 ;• 小熊派实现鸿蒙开机界面(LiteOS+LVGL):链接 ;本节的教程就是先通过 STM32CubeMX 来配置 小熊派的 TFT 初始化代码,开启 DMA 加速(不开启会卡出翔),配置完成后获取 LVGL 的代码,移植到工程里面,然后将 TFT 驱动接口和 LVGL 接口对接,在运行 Demo 代码 二、配置 TFT我们在上一节移植好 LiteOS 工程的基础上使用 CubeMX 配置 TFT 的 SPI 接口,具体 SPI 驱动接口可以参考这篇文章:小熊派 FreeRTOS+SPI+DMA 驱动 TFT-LCD SPI 配置完成如下:开启 DMA,并且在 NVIC 里面使能中断除了上面的 SPI 引脚还需要,配置 TFT 的其他控制引脚,关于引脚在参考文章中有写出来,配置完成如下:在 MDK 工程根目录下创建 Hardware/LCD 文件夹用来存放驱动代码,驱动文件命名为 lcd.c 和 lcd.h拷贝下面的代码进去lcd.c #include "lcd.h"#include "gpio.h"#include "spi.h"#include "cmsis_os.h"extern osSemaphoreId_t DMA_SemaphoreHandle;/* USER CODE BEGIN 1 *//*** @brief SPI 发送字节函数* @param TxData 要发送的数据* @param size 发送数据的字节大小* @return 0:写入成功,其他:写入失败*/uint8_t SPI_WriteByte(uint8_t *TxData,uint16_t size){osStatus_t result;//获取信号,如果上一个DMA传输完成//信号就能获取到,没有传输完成任务就挂起//等到传输完成再恢复result = osSemaphoreAcquire(DMA_SemaphoreHandle,0xFFFF);if(result == osOK){//获取成功return HAL_SPI_Transmit_DMA(&hspi2,TxData,size);}else{//获取失败return 1;}}//DMA 传输完成后会调用 SPI传输完成回调函数//在该函数中我们释放信号void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi){if(hspi->Instance == hspi2.Instance)osSemaphoreRelease(DMA_SemaphoreHandle);}/*** @brief 写命令到LCD* @param cmd —— 需要发送的命令* @return none*/static void LCD_Write_Cmd(uint8_t cmd){LCD_WR_RS(0);SPI_WriteByte(&cmd, 1);}/*** @brief 写数据到LCD* @param dat —— 需要发送的数据* @return none*/static void LCD_Write_Data(uint8_t dat){LCD_WR_RS(1);SPI_WriteByte(&dat, 1);}/*** @breif 打开LCD显示背光* @param none* @return none*/void LCD_DisplayOn(void){LCD_PWR(1);}/*** @brief 关闭LCD显示背光* @param none* @return none*/void LCD_DisplayOff(void){LCD_PWR(0);}/*** @brief 设置数据写入LCD显存区域* @param x1,y1 —— 起点坐标* @param x2,y2 —— 终点坐标* @return none*/void LCD_Address_Set(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2){/* 指定X方向操作区域 */LCD_Write_Cmd(0x2a);LCD_Write_Data(x1 >> 8);LCD_Write_Data(x1);LCD_Write_Data(x2 >> 8);LCD_Write_Data(x2);/* 指定Y方向操作区域 */LCD_Write_Cmd(0x2b);LCD_Write_Data(y1 >> 8);LCD_Write_Data(y1);LCD_Write_Data(y2 >> 8);LCD_Write_Data(y2);/* 发送该命令,LCD开始等待接收显存数据 */LCD_Write_Cmd(0x2C);}/*** @brief 以一种颜色清空LCD屏* @param color —— 清屏颜色(16bit)* @return none*/void LCD_Clear(uint16_t color){uint16_t i;uint8_t data[2] = {0}; //color是16bit的,每个像素点需要两个字节的显存/* 将16bit的color值分开为两个单独的字节 */data[0] = color >> 8;data[1] = color;LCD_Address_Set(0, 0, LCD_Width - 1, LCD_Height - 1);LCD_WR_RS(1);for(i=0;i<((LCD_Width)*(LCD_Height));i++){SPI_WriteByte(data, 2);}}/*** @brief LCD初始化* @param none* @return none*/void LCD_Init(void){/* 复位LCD */LCD_PWR(0);LCD_RST(0);osDelay(100);LCD_RST(1);osDelay(120);/* 关闭睡眠模式 */LCD_Write_Cmd(0x11);osDelay(120);/* 开始设置显存扫描模式,数据格式等 */LCD_Write_Cmd(0x36);LCD_Write_Data(0x00);/* RGB 5-6-5-bit格式 */LCD_Write_Cmd(0x3A);LCD_Write_Data(0x65);/* porch 设置 */LCD_Write_Cmd(0xB2);LCD_Write_Data(0x0C);LCD_Write_Data(0x0C);LCD_Write_Data(0x00);LCD_Write_Data(0x33);LCD_Write_Data(0x33);/* VGH设置 */LCD_Write_Cmd(0xB7);LCD_Write_Data(0x72);/* VCOM 设置 */LCD_Write_Cmd(0xBB);LCD_Write_Data(0x3D);/* LCM 设置 */LCD_Write_Cmd(0xC0);LCD_Write_Data(0x2C);/* VDV and VRH 设置 */LCD_Write_Cmd(0xC2);LCD_Write_Data(0x01);/* VRH 设置 */LCD_Write_Cmd(0xC3);LCD_Write_Data(0x19);/* VDV 设置 */LCD_Write_Cmd(0xC4);LCD_Write_Data(0x20);/* 普通模式下显存速率设置 60Mhz */LCD_Write_Cmd(0xC6);LCD_Write_Data(0x0F);/* 电源控制 */LCD_Write_Cmd(0xD0);LCD_Write_Data(0xA4);LCD_Write_Data(0xA1);/* 电压设置 */LCD_Write_Cmd(0xE0);LCD_Write_Data(0xD0);LCD_Write_Data(0x04);LCD_Write_Data(0x0D);LCD_Write_Data(0x11);LCD_Write_Data(0x13);LCD_Write_Data(0x2B);LCD_Write_Data(0x3F);LCD_Write_Data(0x54);LCD_Write_Data(0x4C);LCD_Write_Data(0x18);LCD_Write_Data(0x0D);LCD_Write_Data(0x0B);LCD_Write_Data(0x1F);LCD_Write_Data(0x23);/* 电压设置 */LCD_Write_Cmd(0xE1);LCD_Write_Data(0xD0);LCD_Write_Data(0x04);LCD_Write_Data(0x0C);LCD_Write_Data(0x11);LCD_Write_Data(0x13);LCD_Write_Data(0x2C);LCD_Write_Data(0x3F);LCD_Write_Data(0x44);LCD_Write_Data(0x51);LCD_Write_Data(0x2F);LCD_Write_Data(0x1F);LCD_Write_Data(0x1F);LCD_Write_Data(0x20);LCD_Write_Data(0x23);/* 显示开 */LCD_Write_Cmd(0x21);LCD_Write_Cmd(0x29);/*打开显示*/LCD_PWR(1);}lcd.h#include "main.h"#define LCD_PWR(n) (n?\HAL_GPIO_WritePin(LCD_PWR_GPIO_Port,LCD_PWR_Pin,GPIO_PIN_SET):\HAL_GPIO_WritePin(LCD_PWR_GPIO_Port,LCD_PWR_Pin,GPIO_PIN_RESET))#define LCD_WR_RS(n) (n?\HAL_GPIO_WritePin(LCD_WR_RS_GPIO_Port,LCD_WR_RS_Pin,GPIO_PIN_SET):\HAL_GPIO_WritePin(LCD_WR_RS_GPIO_Port,LCD_WR_RS_Pin,GPIO_PIN_RESET))#define LCD_RST(n) (n?\HAL_GPIO_WritePin(LCD_RST_GPIO_Port,LCD_RST_Pin,GPIO_PIN_SET):\HAL_GPIO_WritePin(LCD_RST_GPIO_Port,LCD_RST_Pin,GPIO_PIN_RESET))//LCD屏幕分辨率定义#define LCD_Width 240#define LCD_Height 240//颜色定义#define WHITE 0xFFFF //白色#define YELLOW 0xFFE0 //黄色#define BRRED 0XFC07 //棕红色#define PINK 0XF81F //粉色#define RED 0xF800 //红色#define BROWN 0XBC40 //棕色#define GRAY 0X8430 //灰色#define GBLUE 0X07FF //兰色#define GREEN 0x07E0 //绿色#define BLUE 0x001F //蓝色#define BLACK 0x0000 //黑色uint8_t SPI_WriteByte(uint8_t *TxData,uint16_t size);static void LCD_Write_Cmd(uint8_t cmd);static void LCD_Write_Data(uint8_t dat);void LCD_DisplayOn(void);void LCD_DisplayOff(void);void LCD_Address_Set(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2);void LCD_Clear(uint16_t color);void LCD_Init(void);代码和文件添加完成后不要忘记添加文件路径,然后我们在主函数中创建一个用于 lcd 显示的任务,初始化 LCD 同时将屏幕初始化为蓝色osThreadId_t lcd_taskHandle;const osThreadAttr_t lcd_task_attributes = {.name = "lcd_task",.stack_size = 512 * 4,.priority = (osPriority_t) osPriorityNormal1,};void Lcd_Task(void *argument);void Lcd_Task(void *argument){LCD_Init();LCD_Clear(BLUE);while(1){osDelay(1000);}}添加 DMA 信号量osSemaphoreId_t DMA_SemaphoreHandle;const osSemaphoreAttr_t DMA_Semaphore_attributes = {.name = "DMA_Semaphore"};初始化信号和 LiteOS:/* USER CODE BEGIN 2 */osKernelInitialize();/* creation of uart_task */DMA_SemaphoreHandle = osSemaphoreNew(1, 1, &DMA_Semaphore_attributes);led_taskHandle = osThreadNew(Led_Task, NULL, &led_task_attributes);lcd_taskHandle = osThreadNew(Lcd_Task, NULL, &lcd_task_attributes);osKernelStart();/* USER CODE END 2 */编译烧写程序,观察现象,屏幕清屏为蓝色,驱动程序跑通了,可以进行下一步:三、LVGL 源码获取获取 lvgl 7.0 版本的源码git clone -b release/v7 https://github.com/lvgl/lvgl.git拉取后代码下面我们在 MDK 工程目录按照下面的格式建立文件夹APP 文件夹用来存放我们编写的 lvgl 应用代码,LVGL 文件夹用来存放 lvgl 的源码,以及接口代码然后我们将刚刚 github 下载的源码拷贝到 LVGL 中,然后把里面 lvgl\examples\porting 文件夹复制到同一目录下,改名为 lvgl_port 文件夹,同时将 lvgl\lv_conf_template.h 也复制到同一目录,并且改名为 lv_conf.h,修改结果如下:然后将 lvgl_port 下面的文件也修改名称为下面的格式:这6个文件是 lvgl 的接口文件,disp 是显示接口、fs 是文件系统接口、indev 是输入接口,下面我们在 MDK 工程里面添加文件和文件路径,添加路径如下:..\Middlewares\LVGL\APP..\Middlewares\LVGL\LVGL\lvgl_port..\Middlewares\LVGL\LVGL\lvgl\src添加文件如下:src 放的文件是下面文件夹的所有 c 文件config 放的是 lvgl 配置头文件:port 放的是 lvgl 的硬件接口文件文件添加完成后我们先配置 lvgl 下的 lv_conf.h 文件,做一些配置,不然直接编译的话会有一堆报错lv_conf.h 文件修改:修改屏幕尺寸适配小熊派:/* Maximal horizontal and vertical resolution to support by the library.*/#define LV_HOR_RES_MAX (240)#define LV_VER_RES_MAX (240)设置屏幕颜色深度,以及颜色存放格式(适配 ST7789芯片):/* Color depth:* - 1: 1 byte per pixel* - 8: RGB332* - 16: RGB565* - 32: ARGB8888*/#define LV_COLOR_DEPTH 16/* Swap the 2 bytes of RGB565 color.* Useful if the display has a 8 bit interface (e.g. SPI)*/#define LV_COLOR_16_SWAP 1设置调节界面缩放比例:/* Dot Per Inch: used to initialize default sizes.* E.g. a button with width = LV_DPI / 2 -> half inch wide* (Not so important, you can adjust it to modify default sizes and spaces)*/#define LV_DPI 60 /*[px]*/设置动态内存大小:/* Size of the memory used by `lv_mem_alloc` in bytes (>= 2kB)*/# define LV_MEM_SIZE (16U * 1024U)关闭使用 GPU:/* 1: Enable GPU interface*/#define LV_USE_GPU 0 /*Only enables `gpu_fill_cb` and `gpu_blend_cb` in the disp. drv- */#define LV_USE_GPU_STM32_DMA2D 0暂时先关闭文件系统:/* 1: Enable file system (might be required for images */#define LV_USE_FILESYSTEM 0编译一下,有一些警告..\Middlewares\LVGL\LVGL\lvgl\src\lv_draw\lv_draw_mask.c(350): warning: #111-D: statement is unreachable这些警告没有任何影响,可以把警告给屏蔽掉,切换到 C/C++选项卡,在 Misc Controls 中填入--diag_suppress=111 把它屏蔽掉如下图所示:编译后改报错就不显示了四、显示接口移植编译通过后,我们下一步就是修改显示接口了,打开 lv_port_disp.c 文件,将开头使能,包括头文件也使能:/*Copy this file as "lv_port_disp.c" and set this value to "1" to enable content*/#if 1修改显示接口,主要关注 void lv_port_disp_init(void) 函数void lv_port_disp_init(void){/*-------------------------* Initialize your display* -----------------------*/disp_init();/*-----------------------------* Create a buffer for drawing*----------------------------*//* LVGL requires a buffer where it internally draws the widgets.* Later this buffer will passed your display drivers `flush_cb` to copy its content to your display.* The buffer has to be greater than 1 display row** There are three buffering configurations:* 1. Create ONE buffer with some rows:* LVGL will draw the display's content here and writes it to your display** 2. Create TWO buffer with some rows:* LVGL will draw the display's content to a buffer and writes it your display.* You should use DMA to write the buffer's content to the display.* It will enable LVGL to draw the next part of the screen to the other buffer while* the data is being sent form the first buffer. It makes rendering and flushing parallel.** 3. Create TWO screen-sized buffer:* Similar to 2) but the buffer have to be screen sized. When LVGL is ready it will give the* whole frame to display. This way you only need to change the frame buffer's address instead of* copying the pixels.* *//* Example for 1) */static lv_disp_buf_t draw_buf_dsc_1;static lv_color_t draw_buf_1[LV_HOR_RES_MAX * 10]; /*A buffer for 10 rows*/lv_disp_buf_init(&draw_buf_dsc_1, draw_buf_1, NULL, LV_HOR_RES_MAX * 10); /*Initialize the display buffer*//* Example for 2) */static lv_disp_buf_t draw_buf_dsc_2;static lv_color_t draw_buf_2_1[LV_HOR_RES_MAX * 10]; /*A buffer for 10 rows*/static lv_color_t draw_buf_2_2[LV_HOR_RES_MAX * 10]; /*An other buffer for 10 rows*/lv_disp_buf_init(&draw_buf_dsc_2, draw_buf_2_1, draw_buf_2_2, LV_HOR_RES_MAX * 10); /*Initialize the display buffer*//* Example for 3) */static lv_disp_buf_t draw_buf_dsc_3;static lv_color_t draw_buf_3_1[LV_HOR_RES_MAX * LV_VER_RES_MAX]; /*A screen sized buffer*/static lv_color_t draw_buf_3_2[LV_HOR_RES_MAX * LV_VER_RES_MAX]; /*An other screen sized buffer*/lv_disp_buf_init(&draw_buf_dsc_3, draw_buf_3_1, draw_buf_3_2, LV_HOR_RES_MAX * LV_VER_RES_MAX); /*Initialize the display buffer*//*-----------------------------------* Register the display in LVGL*----------------------------------*/lv_disp_drv_t disp_drv; /*Descriptor of a display driver*/lv_disp_drv_init(&disp_drv); /*Basic initialization*//*Set up the functions to access to your display*//*Set the resolution of the display*/disp_drv.hor_res = 480;disp_drv.ver_res = 320;/*Used to copy the buffer's content to the display*/disp_drv.flush_cb = disp_flush;/*Set a display buffer*/disp_drv.buffer = &draw_buf_dsc_1;#if LV_USE_GPU/*Optionally add functions to access the GPU. (Only in buffered mode, LV_VDB_SIZE != 0)*//*Blend two color array using opacity*/disp_drv.gpu_blend_cb = gpu_blend;/*Fill a memory array with a color*/disp_drv.gpu_fill_cb = gpu_fill;#endif/*Finally register the driver*/lv_disp_drv_register(&disp_drv);}disp_init() 用来初始化显示屏外设,这里我们在hal初始化中已经初始化完成了,所以删除他下面的代码就是创建一个缓存 buffer,这里 LVGL 提供了三种方式创建缓存:第一种只创建一个缓存区,长度是横轴像素长度的 10 倍,第二种创建两个缓存区,长度都是 横轴的 10 倍,第三种则是创建两个,大小是横轴乘以纵轴,相当于整个屏幕大小,第一种情况,如果我们在写入数据时不能修改,第二种我们在写入一个 buffer 时还可以希尔另外一个 buffer ,可以结合 DMA 加快写入速度,这里我使用第一种下面的代码注册显示驱动,配置其参数:主要就是配置屏幕参数,设置刷新函数,配置缓存区指针,最后注册驱动,这里我们要修改一下刷新屏幕函数static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)修改如下:/* Flush the content of the internal buffer the specific area on the display* You can use DMA or any hardware acceleration to do this operation in the background but* 'lv_disp_flush_ready()' has to be called when finished. */static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p){/*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/int32_t y;LCD_Address_Set(area->x1,area->y1,area->x2,area->y2);LCD_WR_RS(1);//一行一行 DMAfor(y = area->y1; y <= area->y2; y++) {if(osSemaphoreAcquire(DMA_SemaphoreHandle,0xFFFF) == osOK)HAL_SPI_Transmit_DMA(&hspi2,(uint8_t *)color_p,(uint16_t)(area->x2-area->x1+1)*2);color_p += (area->x2-area->x1+1);}/* IMPORTANT!!!* Inform the graphics library that you are ready with the flushing*/lv_disp_flush_ready(disp_drv);}修改代码后,要添加头文件和 dma 信号量声明/********************** INCLUDES*********************/#include "lv_port_disp.h"#include "lcd.h"#include "spi.h"#include "cmsis_os.h"/********************** DEFINES*********************/extern osSemaphoreId_t DMA_SemaphoreHandle;五、Demo 代码在定时器 1 中断中添加 lvgl 的时基更新代码void TIM1_UP_TIM16_IRQHandler(void){/* USER CODE BEGIN TIM1_UP_TIM16_IRQn 0 *//* USER CODE END TIM1_UP_TIM16_IRQn 0 */HAL_TIM_IRQHandler(&htim1);/* USER CODE BEGIN TIM1_UP_TIM16_IRQn 1 */lv_tick_inc(1);/* USER CODE END TIM1_UP_TIM16_IRQn 1 */}在 main.c 的 lcd 任务中添加创建 label 测试的代码这里的测试代码是画两个对角位置的方块,边框颜色都不一样,一个设置的是蓝色,一个是绿色void Lcd_Task(void *argument){LCD_Init();lv_init();lv_port_disp_init();//lvgl 显示接口初始化,放在 lv_init()的后面lv_style_t style1;lv_style_init(&style1);lv_style_set_bg_color(&style1, LV_STATE_DEFAULT,LV_COLOR_BLACK);lv_style_set_border_width(&style1,LV_STATE_DEFAULT, 5);lv_style_set_border_color(&style1,LV_STATE_DEFAULT, LV_COLOR_BLUE);lv_style_t style2;lv_style_init(&style2);lv_style_set_bg_color(&style2, LV_STATE_DEFAULT,LV_COLOR_BLACK);lv_style_set_border_width(&style2,LV_STATE_DEFAULT, 5);lv_style_set_border_color(&style2,LV_STATE_DEFAULT, LV_COLOR_GREEN);lv_obj_t* bgk1 = lv_obj_create(lv_scr_act(), NULL);//创建对象lv_obj_set_pos(bgk1,0,0);lv_obj_set_size(bgk1, 120, 120);//设置覆盖大小lv_obj_add_style(bgk1,LV_STATE_DEFAULT, &style1);lv_obj_t* bgk2 = lv_obj_create(lv_scr_act(), NULL);//创建对象lv_obj_set_pos(bgk2,120,120);lv_obj_set_size(bgk2, 120, 120);//设置覆盖大小 lv_obj_add_style(bgk2,LV_STATE_DEFAULT, &style2);while(1){lv_task_handler();osDelay(1000);}}/* USER CODE END 0 */记得添加相关头文件:/* USER CODE BEGIN Includes */#include "cmsis_os.h"#include "lcd.h"#include "lv_port_disp.h"/* USER CODE END Includes */编译下载代码六、实验现象两个对角小方块,边框一个蓝色一个绿色
-
低功耗串口接受数据异常 一、异常信息 1.1 硬件环境 1.2 软件环境 1.3 问题描述 二、问题分析 三、解决方式 小熊派低功耗串口接受数据异常一、异常信息1.1 硬件环境小熊派 STM32L4 单片机1.2 软件环境STM32CubeMX 6.2.1MDK 5.31.3 问题描述使用 STM32CubeMX 配置 STM32 的 LPUART 后,生成代码使用串口 DMA 接受数据,在开启数据接受后,指定接受的数据长度,接受完成后,串口继续接受数据,在开启下一个串口接受时则会读取之前的数据,例如:串口使能接受 5 个数据,然后上位机发送 ABCDEFG,本次串口 DMA 在接受到 ABCDE 时就会产生接受完成标志,调用回调函数,至于 FG 则不会接受丢弃掉,但我如果在设置接受 2 个数据,这两个数据居然还能接受到在仿真调试里面运行程序时这两个数据不会接受到,很正常,符合逻辑,但一旦实际下载到单片机里面实际运行就会出现错误二、问题分析根据实验现象,仿真是很正常的,但实际下载运行就会出现问题,推测编译器或者生成的代码有问题理论上第一次接受数量达标后,代码会将接受完成标志置位,然后串口的缓存区不会再接受任何数据,出现这种情况的原因可能是接受完成后数据 DMA 和串口没有关闭三、解决方式在调用之前我们将 DMA 的缓存区清空即可,例如我下面的代码,使用 memset 清空缓存区
-
WiFi的历史从802.11的FHFS,DSSS到802.11b的DSSS,到802.11a的OFDM,802.11g的ERP(将OFDM从5G迁移到了2.4G),到802.11n的更宽频带(40MHz)的OFDM技术,到802.11ac的进一步拓宽(80, 80+80,160MHz)的OFDM技术,到802.11ax的更窄的子载波(78.125kHz)的OFDM技术,到讨论中的802.11be的320MHz的OFDM技术。OFDM技术从802.11a,大概2001年的时候,到现在,已经持续演进了20年了,它的抗多径效应,适合无线空间的复杂环境。因为802.11ax(Wi-FI 6)希望覆盖更广的空间,所以把载波宽度进一步变小了。每一代的发展,频带基本是越来越宽,似乎是可以无限的把频宽扩展下去。但是这应该是存在问题的,多样化的需求下,大频宽是可能浪费频段的。毕竟有些地方只需要小的频宽就好了。802.11ax还定义了一个仅支持20MHz的模式,也是瞄准了万物互联的趋势下,小数据,低能耗的搭配。但是802.11be(TB Wi-Fi 7)又把带宽变得更大了,每一代总是希望能更快的。但是单纯更快有什么意思呢。OFDM不止可以分频带,因为网络的特性,还可以分用户,这是Wi-Fi现在越来越看重的,希望越来越多的用户都是使用这个网络,而且还要能用。从802.11ac(2013)开始的Wi-Fi5已经引入了多用户的观念,对应的技术是MU-MIMO。它是这么样的,不同的用户用的是不同的天线。到了802.1ax(Wi-Fi6),支持上行的MU-MIMO,而且还把一个频段同时分配给了多个用户(OFDMA),每个用户最少可以只用2MHz的带宽。一下还把上行和下行都给加进去了。不止如此,还加入了BSS Coloring技术,弱化Exposed Node的问题,提升密集网络覆盖下的并行性。Wi-Fi6有这么多优势,其实它的主要着笔点在于密集用户,密集网络。想想现在Wi-Fi6的设备已经都出来1两年了,但是也并没有多改变生态。其实现在大家也觉得够用了,就像我自己,这种Wi-Fi6的理念对人这个用户来讲真的是有点超前了,哪里去找如此密集的人流和网络?办公场所,体育场?这些场合毕竟是少数。自动化生产车间也许是比较合适的,不过不是人流,而是物流。Wi-Fi7似乎要在这个道路上越走越远,越来越远…
-
# 准备 使用的源码包为华为官方的ascend200AI加速模块的SDK,其下载地址位于:[点击跳转](https://www.hiascend.com/hardware/firmware-drivers?tag=community) 使用的固件与驱动版本为:1.0.9.alpha 压缩包名称为:A200-3000-sdk_20.2.0.zip 将A200-3000-sdk_20.2.0.zip解压后可以看到Ascend310-source-minirc.tar.gz压缩包,这个压缩包里有ascend200AI加速模块的linux内核源码包、设备树及驱动文件等。 # 设备树节点和驱动的匹配 这里不做过多的赘述了,可自行百度。 其spi的设备树节点位于source/dtb/hi1910-fpga-spi.dtsi中,内容如下: ```bash alg_clk: alg_clk { compatible = "fixed-clock"; #clock-cells = 0>; clock-frequency = 200000000>; }; spi_0: spi@130980000{ #address-cells = 1>; #size-cells = 0>; compatible = "hisi-spi"; reg = 0x1 0x30980000 0 0x10000>, 0x1 0x30900000 0 0x1000>; interrupts = 0 322 4>; clocks = &alg_clk>; clock-names = "spi_clk"; num-cs = 2>; id = 0>; status = "ok"; }; ``` spi_0和spi_1节点的compatible 属性会和spi控制器的驱动匹配。(位于source/drivers/dev_plat/spi/spi-hisi.c) ```bash STATIC const struct of_device_id hisi_spi_dt_ids[] = { { .compatible = "hisi-spi" }, { /* sentinel */ } }; ``` ```bash STATIC struct platform_driver hisi_spi_driver = { .driver = { .name = "hisi_spi", .pm = HISI_SPI_PM_OPS, .of_match_table = of_match_ptr(hisi_spi_dt_ids), #ifdef CONFIG_ACPI .acpi_match_table = ACPI_PTR(hisi_spi_acpi_ids), #endif }, .probe = hisi_spi_probe, .remove = hisi_spi_remove, }; ``` 内核会先解析设备树节点,然后驱动程序注册的时候of_match_ptr中的of_device_id会和设备树节点进行匹配。 # spi控制器驱动分析(source/dev_plat/spi/spi-hisi.c) spi_0和spi_1的设备树节点都会匹配上spi控制器驱动,然后执行他的probe函数,由于spi_1节点的状态为disable,所以只有spi_0节点会执行一次probe函数 ```bash int ret; int irq; u32 id = 0; u32 cs_num = 0; u32 rate = 0; struct resource *res = NULL; struct resource *res_iomux = NULL; struct hisi_spi *hs = NULL; struct spi_master *master = NULL; ``` 初始化变量 ```bash ASSERT_RET((pdev != NULL), -EINVAL); dev_info(&pdev->dev, "hi_spi_probe enter..\n"); dev_info(&pdev->dev, "hi:dev name %s, id %d, auto %d, num res %u\n", pdev->name, pdev->id, pdev->id_auto, pdev->num_resources); ``` ASSERT_RET断言,判断platform_device *pdev结构体是否为空。 dev_info和printk类似,在内核启动时打印消息 ```c ret = hisi_spi_get_dts(pdev, &cs_num, &id, &rate); if (ret != 0) { dev_err(&pdev->dev, "hisi_spi_get_dts fail\n"); return -EINVAL; } ``` hisi_spi_get_dts用于获取片选和时钟等信息 ```c STATIC int hisi_spi_get_dts(struct platform_device *pdev, u32 *cs_num, u32 *id, u32 *rate) { int ret; #ifndef CONFIG_ACPI struct clk *clk = NULL; #endif if ((pdev == NULL) || (cs_num == NULL) || (id == NULL) || (rate == NULL)) { return -EINVAL; } #ifdef CONFIG_ACPI ret = device_property_read_u32(&pdev->dev, "num-cs", cs_num); if (ret 0) { dev_err(&pdev->dev, "get num_cs error: %d \n", ret); return -EINVAL; } ret = device_property_read_u32(&pdev->dev, "id", id); if (ret 0) { dev_err(&pdev->dev, "get id error: %d \n", ret); return -EINVAL; } ret = device_property_read_u32(&pdev->dev, "spi_clk", rate); if (ret 0) { dev_err(&pdev->dev, "get clk error : %d\n", ret); return -EINVAL; } #else ret = of_property_read_u32(pdev->dev.of_node, "num-cs", cs_num); if (ret != 0) { dev_err(&pdev->dev, "of_property_read_u32 get num-cs fail\n"); return -EINVAL; } ret = of_property_read_u32(pdev->dev.of_node, "id", id); if (ret != 0) { dev_err(&pdev->dev, "of_property_read_u32 get id fail\n"); return -EINVAL; } clk = clk_get(&pdev->dev, "spi_clk"); if (IS_ERR(clk)) { return PTR_ERR(clk); } *rate = clk_get_rate(clk); clk_put(clk); #endif return 0; } ``` 这个ACPI好像和电源管理有关,内核中全局搜索CONFIG_ACPI也未找到定义,所里这里先不管他。 这个hisi_spi_get_dts会获取设备书中spi节点信息 片选数量cs_num=2 spi节点id=0 spi时钟速率rate = 200000000 接着获取设备树描述中的mem资源 ```c /* get base addr */ res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (unlikely(res == NULL)) { dev_err(&pdev->dev, "invalid resource\n"); return -EINVAL; } ``` 这里要先说一下platform_get_resource这个函数,内核函数定义如下: ```c struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num) { int i; for (i = 0; i dev->num_resources; i++) { struct resource *r = &dev->resource[i]; if (type == resource_type(r) && num-- == 0) return r; } return NULL; } ``` 从第一份资源开始匹配,type用来匹配资源类型num--==0表示你想获得的是第多少份资源 spi0设备树节点: ```c reg = 0x1 0x30980000 0 0x10000>, 0x1 0x30900000 0 0x1000>; interrupts = 0 322 4>; ``` 则spi0的pdev结构体的资源有三个,两个为IORESOURCE_MEM,一个为IORESOURCE_IRQ。这里调用: ```bash res = platform_get_resource(pdev, IORESOURCE_MEM, 0); ``` 获取的是0x1 0x30980000 0 0x10000>他表示spi控制器的内存起始地址为0x30980000 ,大小为0x10000。然后获取irq资源: ```c irq = platform_get_irq(pdev, 0); if (irq 0) { dev_err(&pdev->dev, "platform_get_irq error\n"); return -ENODEV; } dev_info(&pdev->dev, "hi: get irq %d\n", irq); ``` 中断代码中我没有使用过,所以不管他了,接下来是分配spi_master结构体的内存: ```c master = spi_alloc_master(&pdev->dev, sizeof(*hs)); if (master == NULL) { dev_err(&pdev->dev, "spi_alloc_master error.\n"); return -ENOMEM; } ``` 跳入spi_alloc_master里面: ```c static inline struct spi_controller *spi_alloc_master(struct device *host, unsigned int size) { return __spi_alloc_controller(host, size, false); } ``` ```c struct spi_controller *__spi_alloc_controller(struct device *dev, unsigned int size, bool slave) { struct spi_controller *ctlr; if (!dev) return NULL; ctlr = kzalloc(size + sizeof(*ctlr), GFP_KERNEL); if (!ctlr) return NULL; device_initialize(&ctlr->dev); ctlr->bus_num = -1; ctlr->num_chipselect = 1; ctlr->slave = slave; if (IS_ENABLED(CONFIG_SPI_SLAVE) && slave) ctlr->dev.class = &spi_slave_class; else ctlr->dev.class = &spi_master_class; ctlr->dev.parent = dev; pm_suspend_ignore_children(&ctlr->dev, true); spi_controller_set_devdata(ctlr, &ctlr[1]); return ctlr; } EXPORT_SYMBOL_GPL(__spi_alloc_controller); ``` 重点关注这两行: ```c ctlr = kzalloc(size + sizeof(*ctlr), GFP_KERNEL); spi_controller_set_devdata(ctlr, &ctlr[1]); ``` 总的来说这个spi_alloc_master函数初始化了spi_controller结构体的dev和各个参数,将其设置为master而不是slave,分配的是spi_controller结构体加上自定义的hisi_spi结构体大小,其中master->dev->driver_data指针指向后面这片大小为hisi_spi结构体的内存,hisi_spi是各个厂家根据自己的控制器驱动自定义的结构体。然后接下来填充spi_master结构体 ```c /* the spi->mode bits understood by this driver: */ master->mode_bits = SPI_CPHA | SPI_CPOL; master->dev.of_node = pdev->dev.of_node; master->bus_num = pdev->id; master->num_chipselect = cs_num; master->setup = hisi_spi_setup; master->transfer_one_message = hisi_spi_transfer_one_message; master->cleanup = hisi_spi_cleanup; ``` spi模式默认使用SPI_MODE_3,即clock信号默认为高电平,data信号在clock的第二个跳变沿解析数据。我们使用的是spi0,他的id为0,所以这里的bus_num为0,num_chipselect 为2 hisi_spi_transfer_one_message这个函数,这是spi收发数据的函数。具体在下面讲。 ```c platform_set_drvdata(pdev, master); ``` master是经过上面的spi_alloc_master函数malloc出来的地址,经过platform_set_drvdata后pdev->dev->driver_data指向这片地址,需要的时候可以使用platform_get_drvdata在拿出来使用 ```c hs = spi_master_get_devdata(master); ASSERT_RET((hs != NULL), -ENODEV); ``` spi_master_get_devdata这个函数就是我们上面提到过的,spi_alloc_master分配的除了spi_controller 以外的大小为hisi_spi结构体的大小。 ```c spin_lock_init(&hs->lock); hs->flags = 0; hs->phybase = res->start; hs->pdev = pdev; hs->spi_id = id; hs->clk_rate = (unsigned long)rate; hs->n_bytes = 1; hs->dfs = 8; hs->regs = ioremap(res->start, resource_size(res)); if (hs->regs == NULL) { dev_err(&pdev->dev, "ioremap error.\n"); ret = -ENOMEM; goto out_ioremap; } hs->irq = irq; ret = request_irq(irq, hisi_spi_interrupt, 0, dev_name(&pdev->dev), master); if (ret) { dev_err(&pdev->dev, "request_irq error %d.\n", ret); goto out_irq; } dev_info(&pdev->dev, "hi: get clk rate %d\n", rate); ``` 填充hisi_spi结构体,初始化自旋锁,设备clk_rate,和寄存器地址等内容。 ```c res_iomux = platform_get_resource(pdev, IORESOURCE_MEM, 1); if (res_iomux != NULL) { ret = hisi_spi_iomux_init(hs, res_iomux->start); if (ret) { dev_err(&pdev->dev, "hi_spi_iomux_init error %d.\n", ret); goto out_irq; } } ``` res_iomux获取的是第2个IORESOURCE_MEM资源,即设备树中的0x1 0x30900000 0 0x1000>,然后执行hisi_spi_iomux_init函数来初始化spi的复用功能 ```c STATIC int hisi_spi_iomux_init(struct hisi_spi *hs, unsigned long addr) { void __iomem *regs = NULL; regs = ioremap(addr, IO_MUX_MEM_REMAP_SIZE); if (regs == NULL) { dev_err(&hs->pdev->dev, "remap mem failed.\n"); return -EBUSY; } if (hs->spi_id == 0) { writel_relaxed(FUNC_CTRL_REG_SEL_DS1, regs + SPI1_CLK_FUNC_CTRL_REG); } else if (hs->spi_id == 1) { writel_relaxed(FUNC_CTRL_REG_SEL_DS1, regs + SPI2_CLK_FUNC_CTRL_REG); } else if (hs->spi_id == 2) { writel_relaxed(IOMUX_REG_SEL_SPI2, regs + IOMUX_REG_SPI2_CLK); writel_relaxed(IOMUX_REG_SEL_SPI2, regs + IOMUX_REG_SPI2_CSN); writel_relaxed(IOMUX_REG_SEL_SPI2, regs + IOMUX_REG_SPI2_MOSI); writel_relaxed(IOMUX_REG_SEL_SPI2, regs + IOMUX_REG_SPI2_MISO); writel_relaxed(FUNC_CTRL_REG_SEL_DS2 | FUNC_CTRL_REG_SEL_ST, regs + SCL2_FUNC_CTRL_REG); writel_relaxed(FUNC_CTRL_REG_SEL_DS0 | FUNC_CTRL_REG_SEL_ST | FUNC_CTRL_REG_SEL_RESERVE1 | FUNC_CTRL_REG_SEL_PD | FUNC_CTRL_REG_SEL_PU, regs + SDA2_FUNC_CTRL_REG); writel_relaxed(FUNC_CTRL_REG_SEL_DS0 | FUNC_CTRL_REG_SEL_ST | FUNC_CTRL_REG_SEL_RESERVE1 | FUNC_CTRL_REG_SEL_PD | FUNC_CTRL_REG_SEL_PU, regs + UART1_RXD_FUNC_CTRL_REG); writel_relaxed(FUNC_CTRL_REG_SEL_DS1, regs + UART1_TXD_FUNC_CTRL_REG); } else { dev_err(&hs->pdev->dev, "invalid spi id %d\n", hs->spi_id); } iounmap(regs); regs = NULL; return 0; } ``` 由于我们spi_1是disable,只使用了spi_0,所以这个函数主要是执行了下面这一句 ```c writel_relaxed(FUNC_CTRL_REG_SEL_DS1, regs + SPI1_CLK_FUNC_CTRL_REG); //spi-hisi.h #define FUNC_CTRL_REG_SEL_DS1 (1 5) #define SPI1_CLK_FUNC_CTRL_REG 0x814 ``` 就是把SPI1_CLK_FUNC_CTRL_REG 寄存器的第五位置了1,这里应该是他的复用功能寄存器复用为了spi。 ```c ret = hisi_spi_hw_init(hs); if (ret) { dev_err(&pdev->dev, "hisi_spi_hw_init error %d.\n", ret); goto out_spi; } ``` ```c STATIC int hisi_spi_hw_init(struct hisi_spi *hs) { u32 val; hisi_spi_disable_chip(hs); /* RX_SAMPLE_DLYΪ2 */ spi_writel(hs, RX_SAMPLE_DLY, RXD_DELAY_CYCLE); spi_writel(hs, TXFTLR, 63); spi_writel(hs, RXFTLR, 63); val = 0x3a; spi_writel(hs, IMR, val); hisi_spi_enable_chip(hs); return 0; } ``` 然后是初始化spi控制器的寄存器,SPI_SSIENR(0x0008)好像是他所有的spi控制器的使能寄存器,初始化之前要写0disable,完成后在写1enable。然后对输入采样的延时周期设置,然后是对发送和接收fifo的设置,都设置为63个字节。(怪不得当时使用的时候发现每次传输超过64个字节cs就自动拉高了,用示波器点了好久,原来问题在这里,这里可能和他们硬件有关,fpga写外设时序的时候就给发送和接受缓冲区分配了63个字节),然后是设置中断屏蔽寄存器,也先不管他。 ```c ret = spi_register_master(master); if (ret) { dev_err(&pdev->dev, "spi_register_master error %d.\n", ret); goto out_spi; } ``` spi_register_master这个函数我没有看很明白,但是他是spi控制器驱动必须的,好像只要功能是向linux内核注册这个控制器,让设备驱动能找到这个控制器,经过这个函数后就能在/sys/bus/spi/device目录下找到设备以及他的子节点了。我们可以跳进去简单的看一下 ```c int spi_register_controller(struct spi_controller *ctlr) { 。。。 。。。 /* register the device, then userspace will see it. * registration fails if the bus ID is in use. */ dev_set_name(&ctlr->dev, "spi%u", ctlr->bus_num); status = device_add(&ctlr->dev); if (status 0) { /* free bus id */ mutex_lock(&board_lock); idr_remove(&spi_master_idr, ctlr->bus_num); mutex_unlock(&board_lock); goto done; } dev_dbg(dev, "registered %s %s\n", spi_controller_is_slave(ctlr) ? "slave" : "master", dev_name(&ctlr->dev)); /* * If we're using a queued driver, start the queue. Note that we don't * need the queueing logic if the driver is only supporting high-level * memory operations. */ if (ctlr->transfer) { dev_info(dev, "controller is unqueued, this is deprecated\n"); } else if (ctlr->transfer_one || ctlr->transfer_one_message) { status = spi_controller_initialize_queue(ctlr); if (status) { device_del(&ctlr->dev); /* free bus id */ mutex_lock(&board_lock); idr_remove(&spi_master_idr, ctlr->bus_num); mutex_unlock(&board_lock); goto done; } } /* add statistics */ spin_lock_init(&ctlr->statistics.lock); mutex_lock(&board_lock); list_add_tail(&ctlr->list, &spi_controller_list); list_for_each_entry(bi, &board_list, list) spi_match_controller_to_boardinfo(ctlr, &bi->board_info); mutex_unlock(&board_lock); /* Register devices from the device tree and ACPI */ of_register_spi_devices(ctlr); acpi_register_spi_devices(ctlr); done: return status; } ``` 首先spi_controller_check_ops、spi_controller_is_slave检查传入的spi_controller 结构体是否设置正确,是否设置transfer、transfer_one_message等函数,重点关注下spi_controller_initialize_queue和of_register_spi_devices这两个函数: ```c static int spi_controller_initialize_queue(struct spi_controller *ctlr) { int ret; ctlr->transfer = spi_queued_transfer; if (!ctlr->transfer_one_message) ctlr->transfer_one_message = spi_transfer_one_message; /* Initialize and start queue */ ret = spi_init_queue(ctlr); if (ret) { dev_err(&ctlr->dev, "problem initializing queue\n"); goto err_init_queue; } ctlr->queued = true; ret = spi_start_queue(ctlr); if (ret) { dev_err(&ctlr->dev, "problem starting queue\n"); goto err_start_queue; } return 0; err_start_queue: spi_destroy_queue(ctlr); err_init_queue: return ret; } ``` 将spi_controller的transfer 函数设置为spi_queued_transfer,下面在一开始的probe将transfer_one_message设置为了hisi_spi_transfer_one_message,所以不用管他。spi_init_queue函数初始化并且建立了一个工作线程,并且给该线程指定了一个工作函数spi_pump_messages,spi_start_queue将线程工作标注running置为true。 在spi的设备驱动中会调用master->transfer,则spi_queued_transfer函数会被调用,spi_queued_transfer将设备驱动封装的spi_message加入到master的queue链表中,然后唤醒工作函数,执行spi_pump_messages,在该函数中会执行: ```c ret = ctlr->transfer_one_message(ctlr, ctlr->cur_msg); ``` 即调用了前面设置过的hisi_spi_transfer_one_message函数进行数据传输。 ```c //spi_register_master->spi_register_controller->of_register_spi_devices static void of_register_spi_devices(struct spi_controller *ctlr) { struct spi_device *spi; struct device_node *nc; if (!ctlr->dev.of_node) return; for_each_available_child_of_node(ctlr->dev.of_node, nc) { if (of_node_test_and_set_flag(nc, OF_POPULATED)) continue; spi = of_register_spi_device(ctlr, nc); if (IS_ERR(spi)) { dev_warn(&ctlr->dev, "Failed to create SPI device for %pOF\n", nc); of_node_clear_flag(nc, OF_POPULATED); } } } #else static void of_register_spi_devices(struct spi_controller *ctlr) { } #endif ``` 这个for_each_available_child_of_node函数我跳进去大概看了一下。 ```c #define for_each_available_child_of_node(parent, child) \ for (child = of_get_next_available_child(parent, NULL); child != NULL; \ child = of_get_next_available_child(parent, child)) struct device_node *of_get_next_available_child(const struct device_node *node, struct device_node *prev) { struct device_node *next; unsigned long flags; if (!node) return NULL; raw_spin_lock_irqsave(&devtree_lock, flags); next = prev ? prev->sibling : node->child; for (; next; next = next->sibling) { if (!__of_device_is_available(next)) continue; if (of_node_get(next)) break; } of_node_put(prev); raw_spin_unlock_irqrestore(&devtree_lock, flags); return next; } EXPORT_SYMBOL(of_get_next_available_child); static bool __of_device_is_available(const struct device_node *device) { const char *status; int statlen; if (!device) return false; status = __of_get_property(device, "status", &statlen); if (status == NULL) return true; if (statlen > 0) { if (!strcmp(status, "okay") || !strcmp(status, "ok")) return true; } return false; } ``` parent代表的是当前节点,child = of_get_next_available_child(parent, NULL);表示的是第一个子节点,child = of_get_next_available_child(parent, child)表示的是当前子节点的下一个子节点,直到子节点为NULL。__of_device_is_available的意思是如果status属性为空则返回true,即默认该节点是开启的,如果有status,且他的属性是okay或者ok,则也返回ture。否则返回false。那么最上面的for_each_available_child_of_node意思就好理解了。会遍历spi节点下能用的子节点,然后执行of_register_spi_device ```c static struct spi_device * of_register_spi_device(struct spi_controller *ctlr, struct device_node *nc) { struct spi_device *spi; int rc; /* Alloc an spi_device */ spi = spi_alloc_device(ctlr); if (!spi) { dev_err(&ctlr->dev, "spi_device alloc error for %pOF\n", nc); rc = -ENOMEM; goto err_out; } /* Select device driver */ rc = of_modalias_node(nc, spi->modalias, sizeof(spi->modalias)); if (rc 0) { dev_err(&ctlr->dev, "cannot find modalias for %pOF\n", nc); goto err_out; } rc = of_spi_parse_dt(ctlr, spi, nc); if (rc) goto err_out; /* Store a pointer to the node in the device structure */ of_node_get(nc); spi->dev.of_node = nc; /* Register the new device */ rc = spi_add_device(spi); if (rc) { dev_err(&ctlr->dev, "spi_device register error %pOF\n", nc); goto err_of_node_put; } return spi; err_of_node_put: of_node_put(nc); err_out: spi_dev_put(spi); return ERR_PTR(rc); } ``` spi_alloc_device在内核中申请spi_device的内存,of_modalias_node函数将子节点的compatible属性strlcpy 给spi->modalias,of_spi_parse_dt这个函数里面有地方需要关注下: ```c /* Device address */ rc = of_property_read_u32(nc, "reg", &value); if (rc) { dev_err(&ctlr->dev, "%pOF has no valid 'reg' property (%d)\n", nc, rc); return rc; } spi->chip_select = value; /* Device speed */ rc = of_property_read_u32(nc, "spi-max-frequency", &value); if (rc) { dev_err(&ctlr->dev, "%pOF has no valid 'spi-max-frequency' property (%d)\n", nc, rc); return rc; } spi->max_speed_hz = value; ``` spi_device结构体中的spi->chip_select的值,是spi设备树子节点的reg属性,即reg代表片选,这个地方在设备驱动生成设备的时候会用到。总而言之,会生成两个spi_device设备挂载在spi总线上。 后面的就都是一些收尾工作了。hisi_spi_transfer_one_message是发送和接收的函数,我看不下去了,里面的一些发送接收时序和具体的寄存器配置有兴趣自己看一下吧,但是应该只有厂家搞得懂。
-
今天使用liteOS进行串口数据通信,主要是通过UART2连接外部设备进行数据通信发现以下问题,uart1在liteos中是默认就初始化的,且不能为之配置中断回调函数,亲测一向uart发送数据程序就会卡死,并且使用Transmit之类的函数,也不会打印数据,只能使用printf坑啊uart2串口就很uart1不同了,我一上午都在抓头发苦想uart2为啥接收不到数据,直到晚上,发现在初始化的时候liteos并没有初始化uart2坑啊uart2可以使用HAL库中的函数进行数据通信,不受影响我的发送数据的函数是这个,直接操纵寄存器即,当然也可以使用HAL_UART_Trainsmit来发送数据//串口发送一个字节static void MYUSART_SendData(uint8_t data){ while((USART2->ISR&0X40)==0); USART2->TDR = data;}接收数据使用这个,没有写在中断函数里面HAL_UART_Receive(&huart2,(uint8_t *)USART2_RX_BUF,USART2_MAX_RECV_LEN,500);//接收数据我的开发方式比较特殊,发送数据之后立马读取数据,所以没写在中断,虽然我配置了中断,也写了中断回调函数但是我把回调函数注释了,发现还可以运行,奇怪的知识又增加了!!!if (huart->Instance == USART2)// {// // HAL_UART_Receive_IT(&huart2, USART2_RX_BUF, sizeof(USART2_RX_BUF)); //????????// }这是中断回调函数甚至,我不配置中断,也可以运行,真是离谱他妈给离谱开门。。。总结在liteOS中,尽量不要为串口添加中断中断配置文件里都有了,在代码里使能串口就了发送数据可正常使用HAL库或标准库接收数据如果不是太多或者不及时处理的,尽量不要用中断接收最后讲讲我的开发开发环境:iot studio +mdk v5+STM32L431RCT6小熊派开发板+AS608指纹识别模块+liteOS目前已成功修改as608驱动代码,实现了与指纹模块的握手通信,使用的是HAL库如果有类似我这种开发问题的,可以在帖子下留言,问题+需求+邮箱,源代码等我会通过邮箱发送也可以私信我邮箱Njthdsj@163.com,描述您的问题需求,我会尽可能帮助您
-
WiFi的历史从802.11的FHFS,DSSS到802.11b的DSSS,到802.11a的OFDM,802.11g的ERP(将OFDM从5G迁移到了2.4G),到802.11n的更宽频带(40MHz)的OFDM技术,到802.11ac的进一步拓宽(80, 80+80,160MHz)的OFDM技术,到802.11ax的更窄的子载波(78.125kHz)的OFDM技术,到讨论中的802.11be的320MHz的OFDM技术。OFDM技术从802.11a,大概2001年的时候,到现在,已经持续演进了20年了,它的抗多径效应,适合无线空间的复杂环境。因为802.11ax(Wi-FI 6)希望覆盖更广的空间,所以把载波宽度进一步变小了。每一代的发展,频带基本是越来越宽,似乎是可以无限的把频宽扩展下去。但是这应该是存在问题的,多样化的需求下,大频宽是可能浪费频段的。毕竟有些地方只需要小的频宽就好了。802.11ax还定义了一个仅支持20MHz的模式,也是瞄准了万物互联的趋势下,小数据,低能耗的搭配。但是802.11be(TB Wi-Fi 7)又把带宽变得更大了,每一代总是希望能更快的。但是单纯更快有什么意思呢。OFDM不止可以分频带,因为网络的特性,还可以分用户,这是Wi-Fi现在越来越看重的,希望越来越多的用户都是使用这个网络,而且还要能用。从802.11ac(2013)开始的Wi-Fi5已经引入了多用户的观念,对应的技术是MU-MIMO。它是这么样的,不同的用户用的是不同的天线。到了802.1ax(Wi-Fi6),支持上行的MU-MIMO,而且还把一个频段同时分配给了多个用户(OFDMA),每个用户最少可以只用2MHz的带宽。一下还把上行和下行都给加进去了。不止如此,还加入了BSS Coloring技术,弱化Exposed Node的问题,提升密集网络覆盖下的并行性。Wi-Fi6有这么多优势,其实它的主要着笔点在于密集用户,密集网络。想想现在Wi-Fi6的设备已经都出来1两年了,但是也并没有多改变生态。其实现在大家也觉得够用了,就像我自己,这种Wi-Fi6的理念对人这个用户来讲真的是有点超前了,哪里去找如此密集的人流和网络?办公场所,体育场?这些场合毕竟是少数。自动化生产车间也许是比较合适的,不过不是人流,而是物流。Wi-Fi7似乎要在这个道路上越走越远,越来越远…
-
WiFi的历史从802.11的FHFS,DSSS到802.11b的DSSS,到802.11a的OFDM,802.11g的ERP(将OFDM从5G迁移到了2.4G),到802.11n的更宽频带(40MHz)的OFDM技术,到802.11ac的进一步拓宽(80, 80+80,160MHz)的OFDM技术,到802.11ax的更窄的子载波(78.125kHz)的OFDM技术,到讨论中的802.11be的320MHz的OFDM技术。OFDM技术从802.11a,大概2001年的时候,到现在,已经持续演进了20年了,它的抗多径效应,适合无线空间的复杂环境。因为802.11ax(Wi-FI 6)希望覆盖更广的空间,所以把载波宽度进一步变小了。每一代的发展,频带基本是越来越宽,似乎是可以无限的把频宽扩展下去。但是这应该是存在问题的,多样化的需求下,大频宽是可能浪费频段的。毕竟有些地方只需要小的频宽就好了。802.11ax还定义了一个仅支持20MHz的模式,也是瞄准了万物互联的趋势下,小数据,低能耗的搭配。但是802.11be(TB Wi-Fi 7)又把带宽变得更大了,每一代总是希望能更快的。但是单纯更快有什么意思呢。OFDM不止可以分频带,因为网络的特性,还可以分用户,这是Wi-Fi现在越来越看重的,希望越来越多的用户都是使用这个网络,而且还要能用。从802.11ac(2013)开始的Wi-Fi5已经引入了多用户的观念,对应的技术是MU-MIMO。它是这么样的,不同的用户用的是不同的天线。到了802.1ax(Wi-Fi6),支持上行的MU-MIMO,而且还把一个频段同时分配给了多个用户(OFDMA),每个用户最少可以只用2MHz的带宽。一下还把上行和下行都给加进去了。不止如此,还加入了BSS Coloring技术,弱化Exposed Node的问题,提升密集网络覆盖下的并行性。Wi-Fi6有这么多优势,其实它的主要着笔点在于密集用户,密集网络。想想现在Wi-Fi6的设备已经都出来1两年了,但是也并没有多改变生态。其实现在大家也觉得够用了,就像我自己,这种Wi-Fi6的理念对人这个用户来讲真的是有点超前了,哪里去找如此密集的人流和网络?办公场所,体育场?这些场合毕竟是少数。自动化生产车间也许是比较合适的,不过不是人流,而是物流。Wi-Fi7似乎要在这个道路上越走越远,越来越远…
-
Modbus rtu和Modbus tcp两个协议的本质都是MODBUS协议,都是靠MODBUS寄存器地址来交换数据;但所用的硬件接口不一样,Modbus RTU一般采用串口RS232C或RS485/422,而Modbus TCP一般采用以太网口。现在市场上有很多协议转换器,可以轻松的将这些不同的协议相互转换 如:Intesisbox可以把modbus rtu转换成Modbus tcp实际上Modbus协议包括ASCII、RTU、TCP。标准的Modicon控制器使用RS232C实现串行的Modbus。Modbus的ASCII、RTU协议规定了消息、数据的结构、命令和就答的方式,数据通讯采用Maser/Slave方式。Modbus协议需要对数据进行校验,串行协议中除有奇偶校验外,ASCII模式采用LRC校验,RTU模式采用16位CRC校验.ModbusTCP模式没有额外规定校验,因为TCP协议是一个面向连接的可靠协议。TCP和RTU协议非常类似,只要把RTU协议的两个字节的校验码去掉,然后在RTU协议的开始加上5个0和一个6并通过TCP/IP网络协议发送出去即可 Modbus协议定义的寄存器地址是5位十进制地址,即:线圈(DO)地址:00000~09999触点(DI)地址:10000~19999输入寄存器(AI)地址:30000~39999输出寄存器(AO)地址:40000~49999由于上述各类地址是唯一对应的,因此有些资料就以其第一个数字区分各类地址,即:0x代表线圈(DO)类地址,1x代表触点(DI)类地址、 3x代表输入寄存器(AI)类地址、4x代表输出寄存器(AO)类地址。在实际编程中,由于前缀的区分作用,所以只需说明后4位数,而且需转换为4位十六进制地址。简单点说,modbus有四种数据,DI、DO、AI、AODI: 数字输入,离散输入,一个地址一个数据位,用户只能读取它的状态,不能修改。比如面板上的按键、开关状态,电机的故障状态。DO: 数字输出,线圈输出,一个地址一个数据位,用户可以置位、复位,可以回读状态,比如继电器输出,电机的启停控制信号。AI: 模拟输入,输入寄存器,一个地址16位数据,用户只能读,不能修改,比如一个电压值的读数。AO: 模拟输出,保持寄存器,一个地址16位数据,用户可以写,也可以回读,比如一个控制变频器的电流值。无论这些东西被叫做什么名字,其内容不外乎这几种,输入的信号用户只能看不能改,输出的信号用户控制,并可以回读。离散的数据只有一位,模拟的数据有16位。
-
单片机(Single-Chip Microcomputer)是一种集成电路芯片,是采用超大规模集成电路技术把具有数据处理能力的中央处理器CPU、随机存储器RAM、只读存储器ROM、多种I/O口和中断系统、定时器/计数器等功能(可能还包括显示驱动电路、脉宽调制电路、模拟多路转换器、A/D转换器等电路)集成到一块硅片上构成的一个小而完善的微型计算机系统,在工业控制领域广泛应用。从上世纪80年代,由当时的4位、8位单片机,发展到现在的300M的高速单片机
上滑加载中
推荐直播
-
TinyEngine低代码引擎系列.第1讲——低代码浪潮之下,带你走进TinyEngine
2024/11/11 周一 16:00-18:00
李老师 高级前端开发工程师
低代码浪潮之下,带你走进TinyEngine。李旭宏老师将从低代码的发展趋势、TinyEngine的项目介绍,三方物料组件的使用、跨技术栈的使用、源码生成能力的差异性对比等多个方面带大家对TinyEngine低代码引擎有一个更清晰的认知和了解。
即将直播 -
0代码智能构建AI Agent——华为云AI原生应用引擎的架构与实践
2024/11/13 周三 16:30-18:00
苏秦 华为云aPaaS DTSE技术布道师
大模型及生成式AI对应用和软件产业带来了哪些影响?从企业场景及应用开发视角,面向AI原生应用需要什么样的工具及平台能力?企业要如何选好、用好、管好大模型,使能AI原生应用快速创新?本期直播,华为云aPaaS DTSE技术布道师苏秦将基于华为云自身实践出发,深入浅出地介绍华为云AI原生应用引擎,通过分钟级智能生成Agent应用的方式帮助企业完成从传统应用到智能应用的竞争力转型,使能千行万业智能应用创新。
去报名
热门标签