个人成就
- 发布了43篇内容
- 获得了2次赞同
- 获得了5次收藏
个人简介
擅长领域
暂时没有设置哦~
-
STM32 UART、I2C、SPI串口通信对比分析
大家好,我是张飞实战电子张角老师!
所有通信协议,应该都是一个速度、成本的折中,这里的成本包括完成通信所占据的接口资源(或者说需要几根线)以及硬件电路模块设计的复杂度。
我们前面分析过,如果想要实现高速的数据通信,通信双方的时钟是必须同步的,否则可能会出现数据错位的情况,尤其是通信双方本身的时钟频率相差较大的时候。那么自然的,uart作为一种异步通信协议,它的传输速率肯定不会太高,或者说肯定是小于同步通信协议的。反过来说,如果异步通信协议采用采用发送方或者接受方中的一个时钟频率,作为通信的波特率,那么在两者的时钟频率相差较大的情况下,一定会出现数据错配的情况。甚至有可能,每次传递8个数据都不一定传输得了。信号传输的波特率降低了以后,可以降低对通信双方频率相差的要求,反正最后都要把自身的频率和波特率之间进行一次转换,归一到统一的波特率上来。但是归一化之后依然会有误差的,这个我们前面讨论过,所以依然不能每次传递较多的数据。再加上,uart通信协议中,各种辅助位占据了一定的资源(大约25%),数据传递的速度肯定就更慢了。
虽然uart传输的速度相对较慢,但是如果只是单向通信的话,着实相对来说,非常简单。只需要两根线就可以了,一根Tx就行(大多数情况下,通信的双方都是共地的)。
那么现在我们达成了共识,如果要进行高速的串口通信,必须在通信的双方之间进行时钟同步。那么SPI和I2C这两种协议,应运而生了。那么SPI和I2C有什么区别呢?我感觉这两个有点像TCP和UDP的区别一样,TCP有校验和握手机制;UDP则没有,只是不管不顾发数据。具体表现就是I2C是有应答机制的,但是SPI没有,那么自然I2C这种通信协议的传输速率是没有SPI快的。再一个,在I2C通信中,不管是读指令还是写指令,首先进行的是不是寻址呀,找到相应的芯片以后,才能进行下一步的数据传输,是吧。但是SPI就不用搞这个操作,它是通过硬件的片选信号之间指定从机的,从地址寻址上看,它的速度要远比I2C高。还有一个,不管是读数据还是写数据,一般都是还还要再指定寄存器的地址,然后主机才能通过SDA总线去读取从机中的数据,但是SPI一般是直接通过指令读取相应的寄存器,这中间又少了一次寻址的过程,那么有效数据传递的速度相比I2C,肯定是高了许多。
从通信双工的角度来讲,SPI还支持双工通信,那数据传输的速度就更快了,在这个方面作为半双工通信的I2C自然是更比不了。
虽然传输速度,I2C比不上SPI,但是I2C也是有自己的优点呀,那就是占用的端口资源更少。只要两根线就行了,一根数据线,一根时钟线,就足够了,就可以实现双向通信了。但是SPI可不行了,SPI要实现这种双向通信,至少需要四根线,CLK,MOSI,MISO,CSS。俗话说,一分价钱一分货,此言不虚。
I2C和SPI都是可以实现点对多点进行通信的,既然要实现点对多点进行通信,那么就必须解决寻址的问题。SPI,我们说过了,仗着自己数据传输速度快的优点,财大气粗,对每一个从机都安排了一个片选信号线。这种操作,着实占用了不少单片机的接口资源。但是I2C就不行了,因为数据传递的速度相对较慢,自然得节省开支,包括在点对多点的寻址上也是如此。I2C通信协议是通过地址码的方式来解决多机寻址问题的,这个就有点像SPI的片选线。
系统中的所有外围器件都具有一个7位的"从器件专用地址码",其中高4位为器件类型,由生产厂家制定,低3位为器件引脚定义地址,由使用者定义。主控器件通过地址码建立多机通信的机制,因此I2C总线省去了外围器件的片选线,这样无论总线上挂接多少个器件(当然终端在要在有效个数范围内),其系统仍然为简约的二线结构。终端挂载在总线上,有主端和从端之分,主端必须是带有CPU的逻辑模块,在同一总线上同一时刻只能有一个主端,可以有多个从端,从端的数量受地址空间和总线的最大电容 400pF的限制。
基于IIC总线的物理结构,总线上的START和STOP信号必定是唯一的。如果同时又两个设备同时发起数据传送怎么办?这就涉及到时钟同步和SDA仲裁的问题。我们这里注意到SDA和SCL都是开漏输出设计,那么自然SDA和SCL都具有线与功能。在SCL总线上只有所有的设备的SCL都为高的时候,SCL才为高,只要有一个为低,那么SCL就为低。不管怎样,所有从设备设备的SCL总是同步的。要么是高,要么是低。
那么SDA的仲裁问题是怎么回事呢?SDA线的仲裁也是建立在总线具有线“与”逻辑功能的原理上的。节点在发送1位数据后,比较总线上所呈现的数据与自己发送的是否一致。是,继续发送;否则,退出竞争。这里的比较功能是I2C设计的亮点,有了这个功能为基础,SDA的总线仲裁才能是自动执行的。SDA线的仲裁可以保证I2C总线系统在多个主节点同时企图控制总线时通信正常进行并且数据不丢失。总线系统通过仲裁只允许一个主节点可以继续占据总线。
上图是以两个节点为例的仲裁过程。DATA1和DATA2分别是主节点向总线所发送的数据信号,SDA为总线上所呈现的数据信号,SCL是总线上所呈现的时钟信号。当主节点1、2同时发送起始信号时,两个主节点都发送了高电平信号。这时总线上呈现的信号为高电平,两个主节点都检测到总线上的信号与自己发送的信号相同,继续发送数据。第2个时钟周期,2个主节点都发送低电平信号,在总线上呈现的信号为低电平,仍继续发送数据。在第3个时钟周期,主节点1发送高电平信号,而主节点2发送低电平信号。根据总线的线“与”的逻辑功能,总线上的信号为低电平,这时主节点1检测到总线上的数据和自己所发送的数据不一样,就断开数据的输出级,转为从机接收状态。这样主节点2就赢得了总线,而且数据没有丢失,即总线的数据与主节点2所发送的数据一样,而主节点1在转为从节点后继续接收数据,同样也没有丢掉SDA线上的数据。因此在仲裁过程中数据没有丢失。
这里大家注意一下,SCL同步与SDA仲裁是同时发生的,不存在先后问题。这些实现都是由于I2C设备的特殊设计实现的,堪称非常优雅。
-
STM32 C语言“函数”深入剖析
大家好,我是张飞实战电子张角老师!我们今天对C语言函数的概念进行相关的探讨。探讨的思路还是基本按照前面几篇文章的思路来进行,也就是说需要依次回答:是什么,为什么和怎么用的问题。具体到单片机C语言的函数,我们首先要明确函数是一个什么东东?C语言为什么要使用函数?这个问题主要是相对于汇编语言来说的,大家知道汇编语言就没有函数。函数是如何定义和声明的?后面的,就是函数在实际使用过程中相关的问题,比如函数和变量的定义以及声明有什么不同?函数与函数之间如何进行交互,比如参数传递以及函数如何返回计算结果?最后一部分,函数设计可能是C语言程序设计中的关键一环,我这里会总结一些函数设计相关的一些技巧,分享给大家,进一步减少程序设计过程中出错的可能性,提高编程的效率。
我们首先来看,函数是什么呢?首先,函数有一个名字,这个名字大多数情况下,描述了它的基本功能。然后有的函数有参数,有的函数没有参数;有的函数有返回值,有的函数没有返回值。函数的参数,可以理解为函数进行运算时的加工原料;函数的返回值可以理解为函数计算结果。括号中的东西(函数体),可以理解为函数对数据的加工过程,或者理解为计算过程。
从上面的描述来看,函数应该是一个功能的封装。一般我们的C语言程序,是由许许多多封装起来的函数组成的。但是大家如果写过汇编语言的程序,可能就会有感觉,其实汇编语言并没有函数的概念,汇编语言程序就是一系列指令的罗列。那我们不禁要问,为什么C语言要引入函数的概念呢?函数概念的引入是需要解决什么问题?
大家看一下,函数的引入是不是相当于把一个复杂程序的功能实现了拆解呀,具体拆解的颗粒度因人而异,因程序而已。这个拆解的思路,是不是完美体现了分而治之的策略。在编写程序的过程中,有些常用的功能很有可能屡次被调用,我们把这些常用的功能封装成一个一个函数。程序写到这里的时候,只需要调用这个函数就可以了,不用再重新把代码写一遍,这样是不是就可以较大程度的较少代码量呀。
实际上,我们调用的各种库函数,可以说是在编程中一些最常用的功能的集合。这些写好的、已经经过验证的、较为基本的函数模块,可以极大提升程序的开发效率。这个就相当于,建房子时,有好多基本的结构可以拿来直接使用,比如直接拿来一个柱子、一间房子、一个卫生间等等,不需要自己再使用最基本的砖块从头开始搭建。这样建房子(编程序)的效率是不是会猛增呀。
再一个,没有这个函数的封装,如果程序较大、功能较复杂,我们面对的是不是一个超长的代码呀,那读起来是不是就比较费劲。这个就有点类似古代的文言文不分段落、没有标点符号一样,读起来是不是就比较吃力。那么可以说函数这个概念的引入,可以把一些较长的程序切分成了很多个小模块,那是不是极大地增强了程序的可读性。程序的可读性强,那是不是就从侧面降低了程序的开发和维护成本呀,不用消耗那么多的智力资源就能完成任务。
前面,我们从函数功能的重用度或者说代码量的大小,以及函数对程序的可读性增强两个方面阐述了函数存在的必要性。那么C语言毕竟是人类发明的,肯定要从方便人去进行程序开发的角度来进行语言架构的设计,那么自然C语言就会引入函数的概念。从本质上讲,函数的引入就是为了降低程序开发的难度(这个是相对汇编语言而言的),提升程序开发的效率,方便人用更少的时间去做更多的事情。
再一个,我们看一下,函数与函数之间的组装配合,实现了复杂的程序功能,这个过程是不是像极了人类社会中各个组织之间的相互配合。那么我们在进行程序开发的时候,函数的功能拆分怎么样才是相对比较合理呢?我觉得有两个指标,一个是函数之间调用的深度不要太深,太深的调用会影响程序的可读性。对于一个程序来说,不要分太多层,最好尽可能扁平化;第二个,函数与函数之间,功能一定要独立起来或者说功能解耦一定要尽可能彻底,同一层的函数与函数之间的功能没有相互依赖的关系。从图形上来说,这个可能就是一个树形的结构,第一个数不要太高,太高了人爬上去比较困难;第二个分支与分支之间不能相互影响,必须是各自隔离开,这样维护起来才更方便。从这个角度上,看对一个复杂的程序功能进行有效的拆解的过程,有点像行军打仗时,对军队的管理:不同的部分有不同的功能,各个部分之间有联系但不能是依赖(要能够独立行动),每个部分不能太大否则不好管理(尾大不掉),层级不要太多否则容易政令不通。
讲完了函数是什么,以及C语言为什么要使用函数,那么就到了下面一个部分,如何声明及定义函数。函数的定义和声明,有点像全局变量,或者说函数都有全局属性,它的作用域是全局的。既然是全局的,那肯定要有声明和定义,一般情况下声明是放在“xx.h”头文件里面,定义则是放在“xx.c”文件里面。当然,如何一个函数只是在局部使用,并不对外提供服务,那么可以在这个函数名字面前加上关键字“static”。另外,在这个局部特定的文件里面,这个static函数放在了调用它的函数前面,那么这个函数同样也不需要在进行声明了。编译器会自动找到这个函数,并在链接的时候,自动链接。
既然默认的情况下,函数和全局变量都有全局属性,那么它们在声明和定义上有什么区别呢?主要的区别就在这个关键字“extern”上,全局变量声明的时候,一定要加上extern关键字;但是函数声明的时候,则不需要extern关键字(或者说这个可以省略)。至于为什么有这样的区别,可能就是因为函数这种“数据类型”过于复杂,它的定义和声明之间差别极大,定义有函数体存在,对吧,声明却没有。但是对于变量来说,声明和定义是不是没有什么区别呀,比如定义“int a”,声明也用“int a”,两个是不是重复的呀,所以对于变量来说,声明的时候一定要加上“extern”。这样,其实我们对extern这个关键字,也做了一定的总结。
函数的名字,本身就是一个地址,但从这一点上来看,它有点像数组,和结构体等什么的不太一样。那为什么会这样呢?函数的名字为什么要是一个地址呢?这个可能就和单片机执行程序的思路一样,从某一个地址开始开始执行,那么自然函数的名字就是一个地址更加有利于编译系统对程序进行编译。那么既然函数的名字是一个地址,我们的变量类型里面又有一个指针类型变量,那么他们两个肯定会发生关联,那也就是说一个指针变量里面存储了函数的地址值。这个指针,我们称之为函数指针,全称应该是函数类型的指针,用以区别于整数类型的指针、浮点数类型的指针等等。函数类型的指针,它的声明自然是与众不同的,声明的时候就得按照函数的形式来搞,这个是遵循int* p之类的关于指针声明的法则的。与之相关的一个,比较容易引起混淆的概念,是指针函数。其实如果想区分开来,也是蛮简单的,就是一个返回指针数据类型的函数。这里只是函数指针和指针函数放在一起容易混淆而已。
讲完了函数的声明和定义,我们来看一下第三个问题,函数与函数之间怎么怎么进行交互的。这个交互包括两部分,一个调用方给被调用方数据传递,另一个是被调用方返回值给调用方。调用方给被调用方传递数据,其实大家理解起来也比较简单,就是通过函数的参数进行的。函数的参数需要什么类型的数据,调用方要按照约定传过去。被调用方给调用方传递数据,方式就多了。第一种方式,可以通过返回值的方式,把计算结果返回给调用方;第二种方式,则比较隐晦,不是通过计算结果来实现计算结果返回。具体实现的方式,调用方给被调用方传递参数的时候,传递的是地址;被调用方,通过修改这个地址指向的值来进行计算结果的返回。这个地方其实就是值传递和地址传递的区别,值传递的时候,没有办法实现计算结果这样返回。
最后这一部分,我们来讲一讲函数设计的一般技巧,这样可以让我们在程序设计的时候,更少犯错,进一步提高程序编写的效率。
第一个,原则上尽量少使用全局变量。每个源文件负责本身文件的全局变量,同时提供一组对外函数,方便其他函数使用该对函数来访问这个变量,比如“SetValue”、“GetValue”等等,不要直接读写全局变量。尤其是在多线程编程的时候,必须要使用这种方式,并且要对读/写操作加锁。
第二,和这个类似,函数也尽量少使用static类型的变量,这种变量有记忆功能。有记忆功能的函数,其行为可能是不可预测的,因为它的行为可能取决于某种“记忆状态”,这种函数既不利于理解也不利于维护。
第三,我们要避免函数有太多参数,尽量把函数参数控制在4个或者4个以内。过多的函数参数,可能会导致函数的使用成本太高,比如容易把参数的顺序搞错。
第四,函数体的规模尽可能小,比如控制在80行以内或者说一页屏幕要能够看完。这个规则也可以说我们进行功能拆解的时候,要遵守的。当然,特殊的函数除外呀。
第五,我们要在函数体的入口如,对参数的有效性进行检查,尤其是指针参数。
第六,函数的返回值,一定不能是临时变量的地址。这个地址是在栈空间里面的,函数执行结束后,这个地址就是无效的了。那么自然这个地址,不能用作函数的返回值。
第七,如果是地址传递,则尽量在指针前面使用“const”关键词修饰,防止指针被函数内存的计算误修改。当然特殊需求除外。
关于C语言函数相关的知识,我就和大家先探讨到这里。
参考资料:
C语言中为什么要引入函数的概念http://blog.sina.com.cn/s/blog_6fd2803b0100y9fl.html
C语言函数 https://www.cnblogs.com/wucongzhou/p/12498949.html
函数指针与指针函数 https://www.cnblogs.com/nevel/p/6370264.html
定义与声明、头文件和extern总结 https://www.cnblogs.com/liushui-sky/p/7693537.html
C语言深度解剖,陈正冲,北京航空航天大学出版社
-
STM32 C语言预处理深入解析
大家好,我是张飞实战电子张角老师! 我们今天对C语言的预处理指令做一个总结。其实C语言的预处理指令,主要有三大类,文件包含、宏定义和条件编译。我们首先要明白什么是预处理?第二个C语言编译之前,为什么需要预处理或者说这些预处理的添加能够给C语言的编程带来什么好处?再一个,我们就展开讲解一下C语言常用的三种预处理指令,看看他们如何使用以及有各自有什么优缺点。
所谓预编译或者说预处理,就是在编译之前进行的一些操作,这其中比较主要的可能就是展开操作。那么C语言为什么要添加这些预编译指令呢?从设计者的角度上来看,这些指令肯定会对C语言的编程带来效率的提升,C语言才会保留这些关键字。因为整体上各个预处理指令之间的关联性比较小,我们这里没有办法给他们的优点和缺点做一个统一的定论,这里只能是展开来讲一个一个仔细讲解。
第一个我们我来看一下,头文件指令“#include”。那么在回答这个文件之前,我们要先讨论一下头文件存在的必要性,或者说为什么C语言要分为头文件和源文件。为什么C语言不能像C#和Java那样,不需要头文件么?
第一个我们从C语言发展的历史来看或者说从共享代码对存储空间的使用量上来看,C语言头文件的必要性。之所以C语言一定需要头文件,一个最本质的原因,是因为C编译器编译出来的二进制码(.o, .obj, .lib, .dll)不包括自我描述的符号信息,那么要复用这种可执行代码的话,需要另外的文件。C#和Java的可执行代码可以自带元数据信息,但是这也意味着运行时的内存需求的增加,毕竟这种自我描述的数据对最终用户来说是无用的。当然在C#和Java被发明的时候,存储空间以及很便宜了。但是C被发明的时候,64K的内存是400多美金。一个程序经常几十个模块,运行的程序为某给模块去浪费几十K是不可能的,价格太高了。那么头文件也就应运而生了,可执行代码的元数据信息是在头文件中保存的,头文件可以充当这些模块复用的桥梁,那么进而就可以降低可执行模块复用时对存储空间的需求。
第二,我们从编程器的角度来看一下。C时代的时候编译器比较简单,是固定的编译和链接两个过程,编译一次只处理一个文件,进行预处理之后,头文件会插入到这一个文件里,不同源代码文件的处理时独立的,这样如果头文件里面定义了一个函数的实现,编译的时候所有引用这个头文件的源码文件,生成的obj里都会有这个符号。而链接是通用的链接程序,从汇编时代就用的工具,没有什么高级功能,同一个符号链接时出现两次是会报错的。但是,我们又说了,每个文件的编译是独立的,所以如果实现不在当前源文件里面,调用的时候编译器就不知道这个函数的类型和签名,没法生成调用代码,所以必须在调用之前先声明一遍。如果不把声明写在头文件里面,就必须在每个用到这个函数的源文件里都声明一遍,很不方便,所以综合之后的解决方案就是实现写源码文件里面,声明写头文件里面。
最后,我们从知识产权保护的角度上来分析一下。接口和实现分立之后,使用者只需要关注接口就行了,按照接口的说明使用就好了,不用关注具体是如何实现的。那么作为开发者呢,则可以对实现部分进行加密,那样是不是就可以保护一定的商业秘密了。具体到头文件来说,我们在头文件中进行的函数声明,其实就有这个作用。
也就是说基于以上各种原因,头文件对于C语言来说,是一个确定的存在。那么就肯定要有一个关键字来处理头文件,那这个关键字就是“#include”。在程序正式编译之前,预处理程序会首先展开头文件中的函数以及变量声明。然后程序在编译的时候,对于没有在本文件中实现的变量或者函数,就会做好标记。然后在链接阶段,就会根据类型、名称信息,去其他可执行文件中去找在该文件中没有实现的同时声明过的变量或者函数,如果找到了,而且不重复,那么程序的编译就能够通过。如果没找到,或者找到两个,那么编译就不会成功。
具体到#include的用法,这里还有区别。#include <xx.h>,这种尖括号写法,预编译程序只会到系统指定的目录中去找这些头文件,看看他们是否存在。#include “xx.h”,这种写法,则不是,这种双引号类型的写法,预编译程序会首先到工程目录去找相应的头文件,如果找不到,预处理程序会再到系统指定的目录里面去找。那也就是说,如果是自己写的头文件,我们一定需要使用#include “xx.h”的写法;如果是系统头文件,两种写法都行,但是#include <xx.h>这种写法,效率应该会更高一些。
讲完了文件包含,我们看一下宏定义的预处理指令#define。这个指令大家应该都比较常见。使用宏定义,可以带来不少优点,比如可以避免意义相对模糊的数字出现,#define PI 3.1415926,那么我们就可以在程序中直接使用PI来代替那么一长串的数字。再一个,这个PI会在预编译的时候,直接进行值替代,并不需要为这些常量提供存储空间,那么就进一步提高了程序的执行效率。但是这个也会有隐患的,编译器这里只是进行展开,并不进行类型检查,与此对比的就是const的类型的变量,使用这个变量就有可能避免潜在的错误。再一个,像下面这个,#define FILE_PATH E:folder11.txt,如果目录较长的话,使用起来是不是非常不方便呀,我们就可以直接使用FILE_PATH这个字符串去去代替较长的目录,这样是不是就方便了许多。
虽然#define,给我们带来了许多便利,但是使用它的时候,一定要小心再小心,否则编程的时候,就比较容易出错。
#define PCHAR char*
PCHAR p1,p2;
char** a = &p1;
char** b = &p2
像这段代码,其实就有问题。这里p2其实是char类型,并不是char*类型。对于这种类型的错误,可以使用typedef的方式来解决。
我们再举一个例子,#define MUL(A,B) A*B。而在使用的时候,这样的调用:int a=1,b=2,c=3,d=0;d=MUL(a+b,c),经过编译时候展开,就变成了d=a+b*c,而不是我们所希望的d=(a+b)*c。其解决办法也很简单,就是给每个分量,都加上括号,就可以避免此类问题。即,在宏定义的时候,如此定义:#define MUL(A,B) ((A)*(B))。不过有些时候,加了括号,也没有办法避免这些问题。
但是使用#define来定义函数,确实可以减少系统开销,提高运行效率。为什么会这样呢?因为在C语言中,发生函数调用的时候,需要保留调用函数的现场,子函数执行完毕以后还有回复函数调用的现场,这都需要一定的时间。如果子函数执行的任务比较多,这点时间是可以忽略的,但是如果子函数的功能比较少,比如只是一个加法的操作,那这部分转换操作的开销就太大了。使用带参数的宏定义就不会出现这样的问题,因为他是在预处理阶段就进行了宏展开,在执行的时候不需要进行转换,即在当地执行。宏定义可以完成简单的操作,但是复杂的操作还是要借助函数调用来实现。另外,宏定义的代码如果比较长,预编译的时候,所有引用的部分的代码都需要展开,那么目标代码的空间就会相对较大。所以,我们还是要根据实际情况来决定是否使用宏定义。
实际上,针对宏定义函数的一些缺点,C++引入了inline函数这个关键字。inline函数,可以像宏定义的函数一样,直接展开,而不会发生函数调用的开销。但是呢,inline函数,本质上就是函数,会有参数以及返回执行类型的检查,使用的时候,相对要安全很多。
讲完了宏定义,我们就到了条件编译的部分。所谓条件编译,就是满足条件的时候,就编译,不满足条件的时候,就不编译。条件编译一个很重要的作用,就是可以提高程序的可移植性,我们可以根据不同的平台选择编译不同的程序。条件编译一般有三种格式,这里总结如下。
第一种:
#ifdef 标示符 (注意:标示符一般用#define命令定义)
程序段1
#else
程序段2
#endif第二种:
#ifndef 标示符 (注意:标示符一般用#define命令定义)
程序段1
#else
程序段2
#endif第三种:
#if 表达式 (注意:标示符一般用#define命令定义,如果表达式为真则编译程序1)
程序段1
#else
程序段2
#endif大家在实际使用的时候,参考这三种类型进行使用,就可以了。
我们今天对预编译指令的总结,就先到这里。下一篇文章,我们就谈谈其他的预编译指令。
参考资料:
1. 为什么C/C++要分为头文件和源文件 https://www.zhihu.com/question/280665935
2. C语言预编译的缺点 https://blog.csdn.net/weixin_39632379/article/details/117079935
3. C语言预处理详解 https://blog.csdn.net/czc1997/article/details/81079498
4. #define宏定义的优点和缺点 https://blog.csdn.net/u013910522/article/details/22672057
5. C语言提供的三种预处理命令 https://blog.csdn.net/xiongzebao/article/details/45586743
-
基于RISC-V单片机的鸿蒙开发板项目简介(项目连载1)
大家好,我是张飞实战电子的张角老师。我目前正在做的一个项目是开发一块基于RISC-V架构单片机的鸿蒙系统开发板。
鸿蒙系统是华为公司开发的新一代物联网操作系统系统,RISC-V单片机是不同于ARM架构的单片机,目前形势一片大好。在美国对我国芯片以及操作系统等高新科技的疯狂打压的背景下,鸿蒙系统以及RISC-V架构单片机这两种产品的发展以及突破显得尤其重要。它们两个很大概率上会是电子信息领域我们突破美国技术封锁的两个突破口。
张飞实战电子,愿意在力所能及的情况下,为这两块产品的推广添砖加瓦,贡献自己的力量。为此,我们愿意把这块基于RISC-V架构单片机的鸿蒙系统开发板的实现过程,尽可能清楚的展现给大家,争取越详细越好,为大家以后进行相关的产品开发提供一些经验参考。
这块开发板使用的单片机是深圳睿思芯科的Pygmy-E系列的单片机,这是一款32位的面向IOT领域的单片机,具备超低功耗的特性以及丰富的外设接口。
开发板的硬件电路模块包括电源电路,电平转换电路,单片机最小电路,USB转SPI电路,USB转UART电路,温湿度检测电路,片外SPI Flash电路,红外模块检测电路,开关检测电路,蜂鸣器电路,流水灯电路。
电源电路包括12V电源接口电路,12V转5V Buck电路,5V转3V3 Buck电路,3V3 转1V8 LDO电路,3V3转0V9 buck电路,3V3转0V 8 buck电路。BUCK电源和LDO电源是我们做开发板的过程中非常常用的电源,在这个模块,我将和大家分享下BUCK以及LDO电路的设计方法。
电平转换电路主要是利用双向电平转换芯片TXS0104以及TXB0104等实现电平的双向转换。为什么我们需要电平转换芯片呢,主要是因为我们使用的这块单片机是还处于验证阶段,单片机IO的电平是1V8,而我们常用器件的电平以3V3的居多,比如我们用的FT4222 USB转SPI芯片,SHT20温湿度检测传感器等。
单片机最最小外围电路包括供电模块,晶振模块,复位电路模块,BOOT启动模块等。
其他模块电路我们就不一一介绍了,等以后我们再展开分析。
软件这块,我们打算依据硬件电路开发一些demo。这些demo主要分为三个板块,一个是睿思芯科RISCV单片机的外围驱动开发模块,主要包括GPIO、UART、I2C、SPI等接口的驱动开发;一个是鸿蒙系统Liteos-M模块的内核实验;再一个就是,利用外围电路我们我们要演示的一些demo,比如流水灯,蜂鸣器唱歌,红外信号检测,温湿度检测,外置SPI Flash读取等。
软件的开发和编译是在Ubuntu 中进行的,会使用到一些简单的Linux命令。这里我们会尽可能详细的介绍清楚编译环境的搭建,以及调试过程。
整个项目涉及到的硬件、软件等相关的东西比较庞杂,我们争取尽可能清晰完整地展现所有的细节。因此估计更新的时间会比较长,期待大家能够和我一起坚持到最后。久久为功,方能修成正果。一起加油!
-
基于 RISCV 单片机的鸿蒙开发板-usb转spi硬件电路调试(项目连载2)
基于 RISCV 单片机的鸿蒙开发板
本项目从这个地方开始连载,连载结束后,会把原理图(pdf 版)开源给大家。刚开始大家可能会觉得顺序有些凌乱,主要是因为这个连载不是从最初阶段开始的,后面我会把缺少的部分,比如原理图板块等等一一补上。这样大家看起来,内容就不会显得突兀。
(一) 硬件电路调试
1. usb 转 spi 部分
电路上电后,通过 linux 中的烧录程序烧录往单片机中烧录程序不成功。但是以下两个条件基本可以确定 FT4222 这个外设是没有问题的。
①在 Linux 系统中能够识别 FT4222 这个外设(驱动自动安装的)
②在 Linux 系统中连续通过 FT4222 向单片机发送数据,在单片机的 MOSI 引脚处可以通过示波器实测到信号。如图所示:
那么问题现在最有可能出现的就是单片机这个地方了,先从以下几个地方入手,看看单片机有没有正常工作。
1. 供电
2. 时钟,包含 refclk 和rtcclk 3.test io clk
4. 检查 boot 设置
5. 复位电路
上面几个指标也就是单片机的最小系统必备的部分。供电部分电压示波器测量如下:
1. 1.8V
2. 0.8V
3. 0.9V
4. 3.3V
时钟信号:
1. refclk 24Mhz,pass
1. rtcclk error
查到问题了,焊接电路板的过程中把晶振输出引脚和单片机引脚之间的电阻给“震掉” 了。(此处建议焊接的时候,震动去锡法还是尽量少用,可能会带来未知的风险。。。)
就下面这个电阻(在这耽误了不少时间)
焊接上电阻之后的晶振输出波形:
不过把这个电阻焊接上去之后,烧录依然不成功。那肯定还有其他地方问题没有解决。
1. test io clk
这个频率是 92K,是符合设计需求的,pass。
boot 电路
error,原来 boot 电路部分还没有焊接(为了分步调试,有些电路没有焊接)。赶快把 boot
引脚焊接好。
经查手册,单片机程序烧录的时候,4 号脚应该高,5 号脚为低,也就是 2-3 通,1-4 不通。
在电路板上就是这么一个表现形式,也就是 2 通道 ON,1 通道 OFF。
把上述问题解决后,程序就能够烧进去了,如图: