个人成就
- 发布了50篇内容
- 获得了4次赞同
- 获得了3次收藏
个人简介
擅长领域
暂时没有设置哦~
-
MDK中Buil/ReBuild背后你不知道的故事
在程序开发过程中,大家都会去点击编译按钮,直接开始仿真调试,基本上不怎么关注编译和链接的过程,因为我们使用的工具一般都是厂家做好的集成开发环境(IDE),比如MDK、IAR等。IDE通常将编译和链接合并到一起,虽然 IDE 提供的默认配置、编译和链接参数对于大部分应用程序来说已经足够使用了,但是作为学习,我们可以弄清楚从源代码生成可执行文件的原理。
事实上,从源代码生成可执行文件可以分为四个步骤,分别是预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)和链接(Linking)。下图是生成可执行文件的过程:预处理(Preprocessing)过程主要是处理程序中以#开头的命令,比如 #include、#define等。预处理的规则一般如下:
1.将程序中所有的#define宏定义进行替换。
2.处理程序中所有条件编译命令,比如 #if、#ifdef、#elif、#else、#endif 等。
3.处理#include命令,会将被包含的头文件的内容插入到该预处理命令所在的位置,需要注意的是,这个过程是递归进行的,也就是说被包含的头文件中还可能会包含其他的头文件。
4.删除程序中的注释。
5.添加行号和文件名标识,便于在调试和出错时给出具体的代码位置。
6.保留程序中的#pragma命令,因为编译器需要使用它们。
预处理的结果是生成.i文件。.i文件也包含C语言代码的源文件,只不过所有的宏已经被展开,所有包含的文件已经被插入到当前文件中。当你无法判断宏定义是否正确,或者文件包含是否有效时,可以查看.i文件来确定问题。
编译(Compilation)就是把预处理完的文件进行一些列的词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件(.txt文件)。编译是整个程序构建的核心部分,也是最复杂的部分之一,涉及到的算法较多。在MDK中可以按照以下步骤生产预处理文件和编译文件。勾选之后,再重新Build或者ReBuild就可以了。
汇编(Assembly)的过程就是将上一步生成的汇编指令转换成可以执行的机器指令。汇编过程相对于编译来说比较简单,只是根据汇编语句和机器指令的对照表翻译就可以了。汇编的结果是产生目标文件,即为.o文件。
目标文件已经是二进制文件,与可执行文件的形式类似,只是有些函数和全局变量都是分散的,地址还没有找到,程序不能执行。链接(Linking)的作用就是找到这些目标地址,将所有的目标文件组织成一个可以执行的二进制文件。
以上就是你点击了MDK中的Buil/ReBuild之后,被雪藏的背后的过程,你清楚了吗? -
8脚51单片机DIY时间显示+闹钟技术分享(二)
原理图设计
延续前篇,感谢大家能关注我的实现过程,废话不多说,接下来就是跟大家分享我的原理图设计过程了,首先呢就是我前面说到了DIY效果所以选择用LED来组成数码管,所以我选择用红灯(压降低)来实现,具体做出来就是下面这样的效果了:

是不是感觉还不错呢,主要显示小时和分钟。
因为我选择USB供电,先把供电端设计完成,供电进来我这里加了个保险丝(9V/200mA---大小根据自己实际电路选择),保护整个电路板。
前面设计的时候说到了的模块,我这里再来罗列一下,然后可以通过模块一点点讲实现了。有单片机模块,按键显示模块,时钟芯片模块,蜂鸣器模块。我一个一个来跟大家讲下我的实现过程和感觉需要注意的设计要点。
单片机模块:

电源端就是一个滤波电容一个储能电容。
一个蜂鸣器输出端,其实时钟芯片有触发闹钟时间到的功能,这里我就先把端口连上了,实际后来我实现的时候是通过I2C口读取寄存器获取的。
后面就好理解了,就是两个口是I2C口,另外两个是下载通讯口了,连接出来加个排针就好了。这里我都加了一个10R的电阻是防震荡的作用。到这里呢这个单片机模块部分就完成了,是不是很简单。继续吧。
按键显示模块:

这个就是我前面特别推的一款非常实用的按键显示芯片了。每个段都加了个电阻做限流用的,可以根据实际情况(LED灯的亮度)来调整这个电阻的。这里我们就看到了对于显示和按键我们只用了单片机的I2C两个接口,是不是很强大了。
其实这个芯片还有很多内部处理,比如按键消抖这些,很好了。前面如果下载了我的文章附件的可能大概浏览了下,是不是真的很好呢,希望对你们也有些借鉴。
对于这里的电源端我也加了滤波电容和储能电容。
按键部分我也贴图看下吧,

按键就是用的段管脚,这里这个4.7K的电阻就是一个限流作用了,这里我设计的是共阴使用。
接下来我们看下这个时钟芯片模块部分,还是先贴图看看我的设计吧,然后再看我的设计思路,

先看左边部分,我们看看这个ALARM管脚是不是在单片机管脚的那里连接着呢,就是这个口,那里也说了我最终闹钟实现是通过I2C读取寄存器实现的,这里就不多讲了。这里这个10K电阻是上拉电阻,这个端口是开漏输出,电阻选大点也是为了功耗小点。I2C信号端要加一个4.7K的上拉电阻(这个的话就是看的I2C标准了),再接着看下这个时钟芯片是加外部晶振的,这个是根据手册看的哦。两个电容就是起震需求的电容了。再接着看下供电端,这里我加了个电池,就是在5V不供电的时候,需要电池来给时钟芯片供电,确保时间是对的。所以这里我用了一个二合一二极管,就是为了电池只给时钟芯片供电,防止那个5V的电给电池充电,这里电池我选择的是不可充电的啊。电源端的电容就是滤波用的啊。
到这里时钟芯片部分的设计就完成了,是不是也不难,挺清楚的,如果这部分大家有什么更好的建议欢迎多多交流啊。
接着大家看下我的蜂鸣器部分。还是先上图,再给大家说下我的思路呢,

对于蜂鸣器,我选用的是无源压电式蜂鸣器,我们来看下思路,电源进来,上面我加了一个10R的限流电阻,下面这个R15的作用就是为蜂鸣器放电用的,这个BEEP管脚给一个频率输出到蜂鸣器,让它响出不同的声音,所以这里我用了一个三极管,这个电阻R17的作用就是限流了,下面这个R18就是三极管结电容加速关断的作用了。这样解释一下是不是还是很好理解我的思路的?
做到这里所有思路都讲完了,其实纵观看下还是很容易实现的。原理图设计就这样完成了,大家看下我的有没有什么需要改进的地方呢?实现方法有很多,或者大家有更多更好的方法呢,欢迎来交流啊。后面会陆续更新,下面一篇会展现我的PCB设计过程,感兴趣的持续关注吧。
-
USB的拓扑结构
大家好!我是张飞实战电子蔡琰老师,今天给大家分享USB的拓扑结构。
我们经常使用USB,对USB多少了解呢?本篇文章我们一起来学习一下USB的拓扑结构和数据通信原理。
USB是一种主从结构的系统,主机叫做Host,从机叫做Device,通常所说的主机具有一个或者多个USB主控制器(Host controller)和跟集线器(root hub),主控制器主要负责数据处理,而跟集线器则提供一个连接主控制器和设备之间的接口和通路。另外,还有一类特殊的USB设备-USB集线器,我们也叫USB HUB,它可以对原有的USB口的数量上进行扩展,就可以获得更多的USB口。但是需要注意的是集线器只能扩展处更多的USB口,而不能扩展出更多的带宽,带宽是共享一个USB主控制器的。
USB HUB
通常,PC上有多个USB主控制器和多个USB口,每个主控制器下有一个跟集线器,跟集线器下面通常具有一个或者几个USB口,当你有多个不同的USB设备都需要较大的数据带宽时,可以考虑将他们分别接到不同的主控制器的跟集线器上,以避免带宽不足。
USB的数据交换只能发生在主机与设备之间,主机与主机之间,设备与设备之间不能直接互联和交换数据。为了在物理上区分主机和设备,使用了不同的插头和插座,所有的数据都由主机主动发起,而设备只是被动的负责应答,例如,在读数据时,USB先发出命令,设备收到该命令后,才返回数据。
USB OTG比普通的4线USB多了一条ID识别线,用来表明它是主机还是设备,它可以在主机和设备之间切换角色,这样就实现了设备与设备的链接,增大了USB的使用范围。但是需要注意的是依然没有脱离主从关系,两个设备之间必须要有一个作为主机,一个作为设备。
USB层次连接
塔顶作为USB主控制器和跟集线器,下面接USB集线器,USB集线器将一个USB口扩展为多个USB口,多个USB口又可以通过集线器扩展出更多的接口,但USB协议中对集线器的层数是有限制的,USB1.1规定最多4层,USB2.0规定最多6层,理论上一个USB主控制器最多接127个设备,这是因为协议规定每个USB设备具有一个7bit的地址(取值范围0-127)。
一个完美的数据传输过程如下:首先由USB主控制器发出命令和数据,通过跟集线器,再通过下面的集线器发给USB设备,设备对接收到的数据极性处理后,返回一些信息或者数据,它首先到达上一层的集线器,上层的集线器再交给更上层的集线器,一直到USB的主控制器,最终USB主控制器讲述CPU处理。
文中我们对USB的拓扑结构和数据通信过程做了一个详细的描述,你学到了吗?
-
别再说你的单片机RAM不够用了,来看看这个吧...
当我们写代码的时候,会用到很多变量,如果随意的定义变量,比如写了N多个“unsigned char/int X;”那么代码可能会显的很乱,自己拐回头看的时候都晕掉了,那么这个时候我们可以构造一个复杂的数据类型-结构体类型,对代码中出现的变量进行类别的划分,用构造的结构体类型定义结构体变量,在写or看代码的时候,只要看到这个结构体,就能大致的知道其实现功能,这样看起来就神清气爽了,可读性大大提高。
我们定义的结构体变量,如果没有特殊规定的话是存储在RAM中的,单片机的RAM资源是有限的,那这个结构体变量在RAM中占的空间大小就是我们需要关注一个问题了,它真的像你想的那么“单纯”吗?接下来我们一起来看看吧!
在看下面的图之前,我们说一个前提,在STM32单片机这个32位系统中,signed/unsigned int 占4个字节,signed/unsigned short int 占2个字节 signed/unsigned char 占1个字节,我们称这些为基本数据类型。Size = Sizeof(Test);这个函数是求取这个结构体变量Test所占内存的大小,并返回给Size。
请看上图,我们使用基本数据类型构造了3个复杂的结构体数据类型,仔细看会发现,这3个数据类型的成员可是不大一样的,我们来看第一个Test,这个数据类型总共占4+4=8个字节,这个很好理解,那第二个Test1,占空间大小按道理来说应该是1+4 = 5个字节,但是为什么还是8呢,第三个Test2,占空间大小应该是1+1+4=8,为什么还是8呢?
这个里面就涉及到了结构体对齐,所有的成员在分配内存时都要与所有成员中占内存最多的基本数据类型所占内存空间的字节数对齐。假如这个字节数为 N,那么对齐的原则是:理论上所有成员在分配内存时都是紧接在前一个变量后面依次填充的,但是如果是“以 N 对齐”为原则,那么,如果一行中剩下的空间不足以填充某成员变量时,即剩下的空间小于某成员变量的数据类型所占的字节数,该成员变量在分配内存时另起一行分配。如图3,4:
通过上面的实际测试,我们得出,在构造结构体复杂数据类型的时候,成员变量的排放一定要注意顺序,遵守排放原则,否则就会白白浪费你的空间,掌握好排放原理,能大大提高你的空间利用率。比如我们构造如图5的结构体类型,它依然还是占8个字节。
文末再给大家出个问题,大家看看下面我们构造的数据类型,它们分别占的空间是多大呢?
-
聊一聊内存指针操作
在嵌入式系统中,对内存地址的操作是一个重要的方面,从广义上讲,嵌入式系统的地址空间可以分成以下三种类型:
l 系统的内存
l 处理器内部的寄存器映射
l 处理器外部部件的内存映射
无论哪种内存,一般都映射到处理器的内存空间中。在x86系统中,分为内存和I/O映射两种内存;在ARM体系中,全部的地址都在32位的内存空间中,所有的操作都是对32位地址空间内存的操作。
从编程的角度看,嵌入式系统和PC系统的软件设计的一个重要的区别即在于嵌入式系统更重视对硬件的操作。而对硬件的操作需要通过操作内部寄存器和外围部件内存映射的地址实现,其实现方式都是通过对内存读、写两种操作。
在汇编语言中,各种处理器都有对内存的不同的寻址方式读写内存。在高级语言中,C语言是唯一可以进行内存操作的语言,C语言对内存的操作主要需要通过指针来完成。
1、使用指针操作内存
在C语言中,指针是一种非常重要的数据类型。使用指针变量可以表示各种数据结构,能很方便地使用数组和字符串,并能像汇编一样处理内存地址。指针的本质就是一个地址,在32位的系统中,指针是一个32位的无符号整数。指针可以用一个变量来表示,变量的指针实际上就是变量的地址。存放变量地址的变量是指针变量。一个指针变量的值就是某个变量的地址或称为某变量的指针。
一个简单的指针应用如下所示:
int a;
int *p = &a;
这个例子表示,整型指针型变量p指向a的地址,此时对*p的操作等同于对a的操作。
使用指针可以指向一个变量,也可以指向一个由malloc函数分配的内存,例如:
void *p = malloc(1024);
系统分配1024字节的内存,然后让变量p指向这块内存,即p的值是这1024字节的连续内存的地址。在程序中就可以通过p来操作这块内存区域。在内存使用完成后,需要使用free函数讲内存释放。
free(p);
在嵌入式系统中的程序开发中,指针的值除了以上的两种形式(从系统内存分配或者指向变量)以外,还可以使用绝对的数值。这是由于在嵌入式系统中,外设寄存器和外部部件的内存映射的地址空间可能都是固定的,因此可以使用指针来处理他们。
例如,如果需要在地址0x0040处写入一个字节的数据0xf0,可以使用如下的程序:
unsigned char *p = (unsigned char *)0x0040;
*p = 0xf0;
这个程序定义了一个指向0x0040地址的字节型的指针,然后向该地址写入数据0xf0。
上面的程序等同于:
*(unsigned char *)0x0040 = 0xf0;
所以说不使用指针变量也可以对实际的地址操作。读内存的程序与之类似,可以使用指针变量或者直接使用地址得到内存中的数据。
2、指针的类型
前面的程序在内存的一个指定的地址处写入一个字节(8位)的数据。如果需要写入两个字节(16位)的数据,需要改变指针类型。例如:同样向地址0x0040处写入两个字节的数据0x0f0f,需要使用如下的语句:
*(unsigned short*)0x0040 = 0x0f0f;
在这个语句中,使用unsigned short而不使用unsigned char,short在C语言中代表16位的整数。
如果写入4个字节(32位)的数据,则需要使用一下的程序:
*(unsigned long*)0x0040 = 0xf0f0f0f0;
在这个语句中,使用unsigned long,long在C语言中代表32位的整数。
在32位的系统中,一般编译器认为int代表是32位的整数,等同于long,所以习惯使用int代替long作为内存操作的数据类型。
注意:指针的类型决定了使用指针进行读写操作时每次读写字节的数目。
3、指针的增量
在对指针变量的操作中,有时需要对指针变量进行加减运算。例如:
unsigned char *p = (unsigned char *)0x0040;
P++;
*p = 0xf0;
这段程序的含义是向地址0x0041的字节处写入数据0xf0。
指针加减运算的含义是:指针的单位增量(或减量)等于指针类型所占的内存量。
对指针进行增量操作的使用,增加的单位是以指针类型的大小:char类型的增量表示增加1字节的内存,short表示增加2字节的内存,long和int表示增加4字节的内存,这些工作是编译器根据指针的类型自动完成的。
总结:对指针进行加减运算的时候,它的变化量与指针的类型有关。
4、指针的类型转换
在C语言中,指针的类型可以在使用的时候进行转换。指针的本质是一个地址,在32位系统中,指针就是一个32位无符号的整数。因此,各种指针都可以相互转化,而且指针在转换过程并没有任何实质性的变化,只是告诉编译器,目前的指针指向何种的内存区域。
在嵌入式系统中,处理器的片内设备一般都会映射到处理器的地址空间中。这些寄存器有可能是32位的,有可能只有8位,这时就需要使用C语言中不同类型的指针。
总结:指针的本质是一个无符号的整数,各个类型的指针都可以进行相互转换。
