利用C语言读取BMP文件

@[TOC]什么是bmp文件BMP是bitmap的缩写形式,bitmap顾名思义,就是位图也即Windows位图。它一般由4部分组成:文件头信息块、图像描述信息块、颜色表(在真彩色模式无颜色表)和图像数据区组成。在系统中以BMP为扩展名保存。   打开Windows的画图程序,在保存图像时,可以看到三个选项:2色位图(黑白)、16色位图、256色位图和24位位图。这是最普通的生成位图的工具,在这里讲解的BMP位图形式,主要就是指用画图生成的位图.   一般的bmp图像都是24位,也就是真彩。每8位为一字节,24位也就是使用三字节来存储每一个像素的信息,三个字节对应存放r,g,b三原色的数据每个字节的存贮范围都是0-255。那么以此类推,32位图即每像素存储r,g,b,a(Alpha通道,存储透明度)四种数据。8位图就是只有灰度这一种信息,还有二值图,它只有两种颜色,黑或者白。现在讲解BMP的4个组成部分:1.文件头信息块文件信息头 (14字节)存储着文件类型,文件大小等信息 // 文件信息头结构体 typedef struct tagBITMAPFILEHEADER /unsigned short bfType; // 19778,必须是BM字符串,对应的十六进制为0x4d42,十进制为19778,否则不是bmp格式文件 unsigned int bfSize; // 文件大小 以字节为单位(2-5字节) unsigned short bfReserved1; // 保留,必须设置为0 (6-7字节) unsigned short bfReserved2; // 保留,必须设置为0 (8-9字节) unsigned int bfOffBits; // 从文件头到像素数据的偏移 (10-13字节) } BITMAPFILEHEADER;### 2.图像描述信息块 图片信息头 (40字节)存储着图像的尺寸,颜色索引,位平面数等信息 //图像信息头结构体 typedef struct tagBITMAPINFOHEADER unsigned int biSize; // 此结构体的大小 (14-17字节) long biWidth; // 图像的宽 (18-21字节) long biHeight; // 图像的高 (22-25字节) unsigned short biPlanes; // 表示bmp图片的平面属,显然显示器只有一个平面,所以恒等于1 (26-27字节) unsigned short biBitCount; // 一像素所占的位数,一般为24 (28-29字节) unsigned int biCompression; // 说明图象数据压缩的类型,0为不压缩。 (30-33字节) unsigned int biSizeImage; // 像素数据所占大小, 这个值应该等于上面文件头结构中bfSize-bfOffBits (34-37字节) long biXPelsPerMeter; // 说明水平分辨率,用象素/米表示。一般为0 (38-41字节) long biYPelsPerMeter; // 说明垂直分辨率,用象素/米表示。一般为0 (42-45字节) unsigned int biClrUsed; // 说明位图实际使用的彩色表中的颜色索引数(设为0的话,则说明使用所有调色板项)。 (46-49字节) unsigned int biClrImportant; // 说明对图象显示有重要影响的颜色索引的数目,如果是0,表示都重要。(50-53字节) } BITMAPINFOHEADER;3.颜色表调色板 (由颜色索引数决定)【可以没有此信息】//24位图像素信息结构体,即调色板 typedef struct _PixelInfo { unsigned char rgbBlue; //该颜色的蓝色分量 (值范围为0-255) unsigned char rgbGreen; //该颜色的绿色分量 (值范围为0-255) unsigned char rgbRed; //该颜色的红色分量 (值范围为0-255) unsigned char rgbReserved;// 保留,必须为0 } PixelInfo;### 4.图像数据区 位图数据 (由图像尺寸决定)每一个像素的信息在这里存储  颜色表接下来位为位图文件的图像数据区,在此部分记录着每点像素对应的颜色号,其记录方式也随颜色模式而定,既2色图像每点占1位(8位为1字节);16色图像每点占4位(半字节);256色图像每点占8位(1字节);真彩色图像每点占24位(3字节)。所以,整个数据区的大小也会随之变化。究其规律而言,可的出如下计算公式:图像数据信息大小=(图像宽度图像高度记录像素的位数)/8。 然而,未压缩的图像信息区的大小。除了真彩色模式外,其余的均大于或等于数据信息的大小。这是为什么呢?原因有两个:   1.BMP文件记录一行图像是以字节为单位的。因此,就不存在一个字节中的数据位信息表示的点在不同的两行中。也就是说,设显示模式位16色,在每个字节分配两个点信息时,如果图像的宽度位奇数,那么最后一个像素点的信息将独占一个字节,这个字节的后4位将没有意义。接下来的一个字节将开始记录下一行的信息。   2.为了显示的方便,除了真彩色外,其他的每中颜色模式的行字节数要用数据“00”补齐为4的整数倍。如果显示模式为16色,当图像宽为19时,存储时每行则要补充4-(19/2+1)%4=2个字节(加1是因为里面有一个像素点要独占了一字节)。如果显示模式为256色,当图像宽为19时,每行也要补充4-19%4=1个字节。   还有一点我要申明,当屏幕初始化为16或256色模式时,一定要设置调色板或修正颜色值,否则无法得到正确的图像颜色。说的太抽象,我们现在打开一张bmp文件,来看一看。记住图片的信息:像素是502x179 大小是263KB 所占269986个字节我们用ULtraEdit打开bmp文件。显示的是16进制的代码。现在我们来读取这些代码,看看他们到底保存了一些啥东西。在这里要注意的是Windows的数据是倒着念的,这是PC电脑的特色。如果一段数据为42 4D,倒着念就是4D 42,即0x4D42。因此,如果bfSize的数据为A2 1E 04 00,实际上就成了0x00041EA2,也就是0x41EA2。可以用这个文件中读到以下信息。unsigned short bfType = 0x4D42 = 19778 unsigned int bfSize = 0x41EA2 = 269986字节=269986/1024=263kb unsigned short bfReserved1 = 00 00 unsigned short bfReserved2 = 00 00 unsigned int bfOffBits = 0X00000036 = 0x36 = 54字节 unsigned int biSize = 0x00000028 = 0x28 = 40字节 int biWidth = 0x0001f6 = 0x1f6 = 502像素; int biHeight = 0x000000B3 = 0xB3 = 179像素 ; unsigned short biPlanes = 0x0001 0x1 = 1; unsigned short biBitCount = 0x0018 = 0x18 = 24位; unsigned int biCompression = 0x00000000 = 0; unsigned int biSizeImage = 0x00000000 = 0; int biXPelsPerMeter = 0x00001273 = 0x1273 = 4723; int biYPelsPerMeter = 0x00001273 = 0x1273 = 4723; unsigned int biClrUsed = 0x00000000 = 0; unsigned int biClrImportant = 0x00000000 = 0; unsigned char rgbBlue = 0x496864 其中R:0X64 G:0X68 B:0X49 ; //该颜色的蓝色分量 (值范围为0-255) unsigned char rgbGreen = 0x4A6965 其中R:0X65 G:0X69 B:0X4A ; //该颜色的绿色分量 (值范围为0-255) unsigned char rgbRed = 0x496663 其中R:0X63 G:0X66 B:0X49 ; //该颜色的红色分量 (值范围为0-255) unsigned char rgbReserved; // 保留,必须为0编写代码接下就是要用C语言来读取bmp文件,来具体看一下我们从二进制文本中读到的信息是否和调试的一样。这里你应该了解过C语言的结构体和文件指针。否则这么简单地代码,你可能也看不懂。下面的两段代码你可以直接复制到你的工程,注意这是C环境,不是c++环境。C文件# ifndef BMP_H # define BMP_H BMP格式 这种格式内的数据分为三到四个部分,依次是: 文件信息头 (14字节)存储着文件类型,文件大小等信息 图片信息头 (40字节)存储着图像的尺寸,颜色索引,位平面数等信息 调色板 (由颜色索引数决定)【可以没有此信息】 位图数据 (由图像尺寸决定)每一个像素的信息在这里存储 一般的bmp图像都是24位,也就是真彩。每8位为一字节,24位也就是使用三字节来存储每一个像素的信息,三个字节对应存放r,g,b三原色的数据, 每个字节的存贮范围都是0-255。那么以此类推,32位图即每像素存储r,g,b,a(Alpha通道,存储透明度)四种数据。8位图就是只有灰度这一种信息, 还有二值图,它只有两种颜色,黑或者白。 // 文件信息头结构体 typedef struct tagBITMAPFILEHEADER //unsigned short bfType; // 19778,必须是BM字符串,对应的十六进制为0x4d42,十进制为19778,否则不是bmp格式文件 unsigned int bfSize; // 文件大小 以字节为单位(2-5字节) unsigned short bfReserved1; // 保留,必须设置为0 (6-7字节) unsigned short bfReserved2; // 保留,必须设置为0 (8-9字节) unsigned int bfOffBits; // 从文件头到像素数据的偏移 (10-13字节) } BITMAPFILEHEADER; //图像信息头结构体 typedef struct tagBITMAPINFOHEADER unsigned int biSize; // 此结构体的大小 (14-17字节) long biWidth; // 图像的宽 (18-21字节) long biHeight; // 图像的高 (22-25字节) unsigned short biPlanes; // 表示bmp图片的平面属,显然显示器只有一个平面,所以恒等于1 (26-27字节) unsigned short biBitCount; // 一像素所占的位数,一般为24 (28-29字节) unsigned int biCompression; // 说明图象数据压缩的类型,0为不压缩。 (30-33字节) unsigned int biSizeImage; // 像素数据所占大小, 这个值应该等于上面文件头结构中bfSize-bfOffBits (34-37字节) long biXPelsPerMeter; // 说明水平分辨率,用象素/米表示。一般为0 (38-41字节) long biYPelsPerMeter; // 说明垂直分辨率,用象素/米表示。一般为0 (42-45字节) unsigned int biClrUsed; // 说明位图实际使用的彩色表中的颜色索引数(设为0的话,则说明使用所有调色板项)。 (46-49字节) unsigned int biClrImportant; // 说明对图象显示有重要影响的颜色索引的数目,如果是0,表示都重要。(50-53字节) } BITMAPINFOHEADER; //24位图像素信息结构体,即调色板 typedef struct _PixelInfo { unsigned char rgbBlue; //该颜色的蓝色分量 (值范围为0-255) unsigned char rgbGreen; //该颜色的绿色分量 (值范围为0-255) unsigned char rgbRed; //该颜色的红色分量 (值范围为0-255) unsigned char rgbReserved;// 保留,必须为0 } PixelInfo; #endifh头文件#include <stdio.h> #include <malloc.h> #include "BmpFormat.h" BITMAPFILEHEADER fileHeader; BITMAPINFOHEADER infoHeader; void showBmpHead(BITMAPFILEHEADER pBmpHead) { //定义显示信息的函数,传入文件头结构体 printf("BMP文件大小:%dkb\n", fileHeader.bfSize/1024); printf("保留字必须为0:%d\n", fileHeader.bfReserved1); printf("保留字必须为0:%d\n", fileHeader.bfReserved2); printf("实际位图数据的偏移字节数: %d\n", fileHeader.bfOffBits); void showBmpInfoHead(BITMAPINFOHEADER pBmpinfoHead) {//定义显示信息的函数,传入的是信息头结构体 printf("位图信息头:\n" ); printf("信息头的大小:%d\n" ,infoHeader.biSize); printf("位图宽度:%d\n" ,infoHeader.biWidth); printf("位图高度:%d\n" ,infoHeader.biHeight); printf("图像的位面数(位面数是调色板的数量,默认为1个调色板):%d\n" ,infoHeader.biPlanes); printf("每个像素的位数:%d\n" ,infoHeader.biBitCount); printf("压缩方式:%d\n" ,infoHeader.biCompression); printf("图像的大小:%d\n" ,infoHeader.biSizeImage); printf("水平方向分辨率:%d\n" ,infoHeader.biXPelsPerMeter); printf("垂直方向分辨率:%d\n" ,infoHeader.biYPelsPerMeter); printf("使用的颜色数:%d\n" ,infoHeader.biClrUsed); printf("重要颜色数:%d\n" ,infoHeader.biClrImportant); int main() FILE* fp; fp = fopen("1.bmp", "rb");//读取同目录下的image.bmp文件。 if(fp == NULL) printf("打开'image.bmp'失败!\n"); return -1; //如果不先读取bifType,根据C语言结构体Sizeof运算规则——整体大于部分之和,从而导致读文件错位 unsigned short fileType; fread(&fileType,1,sizeof (unsigned short), fp); if (fileType = 0x4d42) printf("文件类型标识正确!" ); printf("\n文件标识符:%d\n", fileType); fread(&fileHeader, 1, sizeof(BITMAPFILEHEADER), fp); showBmpHead(fileHeader); fread(&infoHeader, 1, sizeof(BITMAPINFOHEADER), fp); showBmpInfoHead(infoHeader); fclose(fp); }记住bmp文件的文件名是1.bmp,这是我的文件名。当然你可以随便设置。而且要把图片和你的代码文件放到同一文件夹。接下来就进行代码调试。我这里用的是vscode调试的代码的。其他的编译器也可以,如果你也想安装vscode请看:10分钟搭建VScode的C/C++开发环境将代码编写完成后就可以按F5,进行调试。可以看到这些信息都是正确的,说明我们的程序是没有错误的,至此我们的通过C语言来实现bmp文件的读取就完成了,接下来我们就要实现bmp文件的读取和保存,具体看我的其他文章。存储算法BMP 文件通常是不压缩的,所以它们通常比同一幅图像的压缩图像文件格式要大很多。例如,一个 800×600 的 24位几乎占据 1.4MB 空间。因此它们通常不适合在因特网或者其它低速或者有容量限制的媒介上进行传输。 根据颜色深度的不同,图像上的一个像素可以用一个或者多个字节表示,它由 n/8 所确定(n 是位深度,1 字节包含 8 个数据位)。图片浏览器等基于字节的 ASCII 值计算像素的颜色,然后从调色板中读出相应的值。更为详细的信息请参阅下面关于位图文件的部分。 N 位 2n 种颜色的位图近似字节数可以用下面的公式计算: BMP 文件大小约等于 54+42 的 n 次方+(wh*n)/8,其中高度和宽度都是像素数。 需要注意的是上面公式中的 54 是位图文件的文件头,是彩色调色板的大小。另外需要注意的是这是一个近似值,对于 n 位的位图图像来说,尽管可能有最多 2n 中颜色,一个特定的图像可能并不会使用这些所有的颜色。由于彩色调色板仅仅定义了图像所用的颜色,所以实际的彩色调色板将小于。 如果想知道这些值是如何得到的,请参考下面文件格式的部分。 由于存储算法本身决定的因素,根据几个图像参数的不同计算出的大小与实际的文件大小将会有一些细小的差别。关注微信公众号:[<font=red>果果小师弟],获取更多精彩内容!

计算机二级C语言知识点总结

C语言二级最重要的知识点总体上必须清楚的:1)程序结构是三种: 顺序结构 、选择结构(分支结构)、循环结构。2)读程序都要从main()入口, 然后从最上面顺序往下读(碰到循环做循环,碰到选择做选择),有且只有一个main函数。3)计算机的数据在电脑中保存是以二进制的形式. 数据存放的位置就是 他的地址.4)bit是位 是指为0 或者1。 byte 是指字节,一个字节 = 八个位概念常考到的:1、编译预处理不是C语言的一部分,不占运行时间,不要加分号。C语言编译的程序称为源程序,它以ASCII数值存放在文本文件中。2、#define PI 3.1415926; 这个写法是错误的,一定不能出现分号。define a 1+2 define a (1+2) a=a*a=1+2*1+2=5 a=a*a=3*3=9 3、每个C语言程序中main函数是有且只有一个。4、在函数中不可以再定义函数。5、算法:可以没有输入,但是一定要有输出。6、break可用于循环结构和switch语句。7、逗号运算符的级别最低,赋值的级别倒数第二。第一章C语言的基础知识第一节、对C语言的基础认识1、C语言编写的程序称为源程序,又称为编译单位。2、C语言书写格式是自由的,每行可以写多个语句,可以写多行。3、一个C语言程序有且只有一个main函数,是程序运行的起点。第二节、熟悉vc++1、VC是软件,用来运行写的C语言程序。2、每个C语言程序写完后,都是先编译,后链接,最后运行。这个过程中注意.c和.obj文件时无法运行的,只有.exe文件才可以运行。(常考!)第三节、标识符1、标识符(必考内容):合法的要求是由字母,数字,下划线组成。有其它元素就错了。并且第一个必须为字母或则是下划线。第一个为数字就错了2、标识符分为关键字、预定义标识符、用户标识符。关键字:不可以作为用户标识符号。main define scanf printf 都不是关键字。迷惑你的地方If是可以做为用户标识符。因为If中的第一个字母大写了,所以不是关键字。预定义标识符:背诵define scanf printf include。记住预定义标识符可以做为用户标识符。用户标识符:基本上每年都考,详细请见书上习题。第四节:进制的转换十进制转换成二进制、八进制、十六进制。二进制、八进制、十六进制转换成十进制。第五节:整数与实数1)C语言只有八、十、十六进制,没有二进制。但是运行时候,所有的进制都要转换成二进制来进行处理。(考过两次)a、C语言中的八进制规定要以0开头。018的数值是非法的,八进制是没有8的,逢8进1。b、C语言中的十六进制规定要以0x开头。2)小数的合法写法:C语言小数点两边有一个是零的话,可以不用写。1.0在C语言中可写成1.0.1在C语言中可以写成.1。3)实型数据的合法形式:a、2.333e-1 就是合法的,且数据是2.333×10-1。b、考试口诀:e前e后必有数,e后必为整数。请结合书上的例子。4) 整型一般是4个字节, 字符型是1个字节,双精度一般是8个字节:long int x; 表示x是长整型。unsigned int x; 表示x是无符号整型。第六、七节:算术表达式和赋值表达式核心:表达式一定有数值!1、算术表达式:+,-,*,/,%考试一定要注意:“/” 两边都是整型的话,结果就是一个整型。 3/2的结果就是1."/” 如果有一边是小数,那么结果就是小数。 3/2.0的结果就是1.5”%”符号请一定要注意是余数,考试最容易算成了除号。 %符号两边要求是整数 。不是整数就错了。2、赋值表达式:表达式数值是最左边的数值,a=b=5;该表达式为5,常量不可以赋值。1、int x=y=10: 错啦,定义时,不可以连续赋值。2、int x,y; x=y=10; 对滴,定义完成后,可以连续赋值。3、赋值的左边只能是一个变量。4、int x=7.7;对滴,x就是75、float y=7;对滴,x就是7.03、复合的赋值表达式:int a=2;a*=2+3;运行完成后,a的值是12。一定要注意,首先要在2+3的上面打上括号。变成(2+3)再运算。复合语句一定用{};4、自加表达式:自加、自减表达式:假设a=5,++a(是为6), a++(为5);运行的机理:++a 是先把变量的数值加上1,然后把得到的数值放到变量a中,然后再用这个++a表达式的数值为6,而a++是先用该表达式的数值为5,然后再把a的数值加上1为6,再放到变量a中。 进行了++a和a++后在下面的程序中再用到a的话都是变量a中的6了。考试口诀: ++在前先加后用,++在后先用后加。5、逗号表达式:优先级别最低。表达式的数值逗号最右边的那个表达式的数值。(2,3,4)的表达式的数值就是4。z=(2,3,4)(整个是赋值表达式) 这个时候z的值为4。(有点难度哦!)z= 2,3,4 (整个是逗号表达式)这个时候z的值为2。补充:1、空语句不可以随意执行,会导致逻辑错误。2、注释是最近几年考试的重点,注释不是C语言,不占运行时间,没有分号。不可以嵌套!3、强制类型转换:一定是 (int)a 不是 int(a),注意类型上一定有括号的。注意(int)(a+b) 和(int)a+b 的区别。前是把a+b转型,后是把a转型再加b。4、三种取整丢小数的情况:1、int a =1.6; 2、(int)a;   3、1/2; 3/2; 4、不丢小数办法,在相应的格式中加前缀.2保留2位,四舍五入第八节、字符1 字符数据的合法形式::'1' 是字符占一个字节,"1"是字符串占两个字节(含有一个结束符号)。'0' 的ASCII数值表示为48,'a' 的ASCII数值是97,'A'的ASCII数值是65。一般考试表示单个字符错误的形式: '65' "1"字符是可以进行算术运算的,记住: '0'-0=48大写字母和小写字母转换的方法: 'A'+32='a' 相互之间一般是相差32。2 转义字符:转义字符分为一般转义字符、八进制转义字符、十六进制转义字符。一般转义字符:背诵\0、 \n、 \’、 \”、 \。八进制转义字符: ‘\141’ 是合法的, 前导的0是不能写的。十六进制转义字符:’\x6d’ 才是合法的,前导的0不能写,并且x是小写。3 字符型和整数是近亲:两个具有很大的相似之处char a = 65 ;  printf(“%c”,a); 得到的输出结果:a printf(“%d”, a); 得到的输出结果:65 第九节、位运算1 位运算的考查:会有一到二题考试题目。总的处理方法:几乎所有的位运算的题目都要按这个流程来处理(先把十进制变成二进制再变成十进制)。例1: char a = 6,b;b = a<<2; 这种题目的计算是先要把a的十进制6化成二进制,再做位运算。例2: 一定要记住,异或的位运算符号” ^ ”。0 异或 1得到1。 0 异或 0得到0。两个女的生不出来。考试记忆方法:一男(1)一女(0)才可以生个小孩(1)。例3: 在没有舍去数据的时候, <<左移一位表示乘以2;>>右移一位表示除以2。   第二章第一节:数据输出(一)(二)1、使用printf和scanf函数时,要在最前面加上#include“stdio.h”2、printf可以只有一个参数,也可以有两个参数。(选择题考过一次)3、printf(“ 第一部分 ”,第二部分 );把第二部分的变量、表达式、常量以第一部分的形式展现出来!4、printf(“a=%d,b=%d”,12, 34) 考试重点!一定要记住是将12和34以第一部分的形式现在在终端也就是黑色的屏幕上。考试核心为:一模一样。在黑色屏幕上面显示为 a=12,b=34。 printf(“a=%d,\n b=%d”,12, 34)那么输出的结果就是:a=12,b=34提示输出错误解决办法(整形数组)1 printf(i==n?”%d ”:”%d\n”,a[i]);2 (i<n-1)printf(“%d ”,a[i]); prinitf(“%d\n”,a[i]);3、int x=017; 一定要弄清楚为什么是这个结果!过程很重要printf(“%d”, x); 15 printf(“%o”, x); 17 printf(“%#o”,x); 017 printf(“%x”, x); 11 printf(“%#x”,x); 0x114、int x=12,y=34; 注意这种题型int x=12,y=34; char z=‘a’; printf(“%d\n ”,x,y); 一个格式说明,两个输出变量,后面的y不输出 printf(“%c\n ”,z); 结果为:12a结果 12 a5、一定要背诵的格式说明表示内容格式说明表示内容%d整型 int%c字符 char%ld长整型 long int%s字符串%f浮点型 float%o八进制%lfdouble%#o带前导的八进制%%输出一个百分号%x%X十六进制输出大写%5d %#x带前导的十六进制举例说明:printf(“%2d”,123 ); 第二部分有三位,大于指定的两位,原样输出123 printf(“%5d”,123 ); 第二部分有三位,小于指定的五位,左边补两个空格 123 printf(“%10f”,1.25 ); 小数要求补足6位的,没有六位的补0,。结果为 1.250000 printf(“%5.3f”,125 ); 小数三位,整个五位,结果为1.250(小数点算一位) printf(“%3.1f”,1.25 );小数一位,整个三位,结果为1.3(要进行四舍五入) 输出时间“%02d:%02d:%02d\n”第三节 数据输入防止非法输入while(~scanf()){}多重用例1 while(scanf()!=EOF) 2 while(scanf(“”,&a),a!=-1){s[n]=a;n++}1、scanf(“a=%d,b=%d”,&a,&b) 考试超级重点!一定要记住是以第一部分的格式在终端输入数据。考试核心为:一模一样。在黑色屏幕上面输入的为 a=12,b=34才可以把12和34正确给a和b 。有一点不同也不行。2、scanf(“%d,%d”,x,y);这种写法绝对错误,scanf的第二个部分一定要是地址!scanf(“%d,%d”,&x,&y);注意写成这样才可以!没地址一开始运行就会bug3、特别注意指针在scanf的考察例如: int x=2;int *p=&x; scanf(“%d”,x); 错误 scanf(“%d”,p); 正确 scanf(“%d”,&p);错误 scanf(“%d”,*p) 错误4、指定输入的长度 (考试重点)终端输入:1234567 scanf(“%2d%4d%d”,&x,&y,&z);x为12,y为3456,z为7终端输入:1 234567 由于1和2中间有空格,所以只有1位给x scanf(“%2d%4d%d”,&x,&y,&z);x为1,y为2345,z为675、字符和整型是近亲:int x=97; printf(“%d”,x); 结果为97 printf(“%c”,x); 结果为 a6、输入时候字符和整数的区别(考试超级重点)scanf(“%d”,&x);这个时候输入1,特别注意表示的是整数1 scanf(“%c”,&x);这个时候输入1,特别注意表示的是字符‘1’ASCII为整数48。补充说明:1)scanf函数的格式考察:注意该函数的第二个部分是&a 这样的地址,不是a; scanf(“%d%d%*d%d”,&a,&b,&c);跳过输入的第三个数据。 2)putchar ,getchar 函数的考查:char a = getchar() 是没有参数的,从键盘得到你输入的一个字符给变量a。 putchar(‘y’)把字符y输出到屏幕中。 3)如何实现两个变量x ,y中数值的互换(要求背下来)不可以把 x=y ,y=x; 要用中间变量 t=x;x=y;y=t。 位运算a=a^b;b=b^a;a=a^b; 4)如何实现保留三位小数,第四位四舍五入的程序,(要求背下来)y=(int)(x*100+0.5)/100.0 这个保留两位,对第三位四舍五入 y=(int)(x*1000+0.5)/1000.0 这个保留三位,对第四位四舍五入 y=(int)(x*10000+0.5)/10000.0 这个保留四位,对第五位四舍五入这个有推广的意义,注意 x = (int)x 这样是把小数部分去掉。第三章特别要注意:C语言中是用非0表示逻辑真的,用0表示逻辑假的。C语言有构造类型,没有逻辑类型。关系运算符号:注意<=的写法,==和=的区别!(考试重点)if只管后面一个语句,要管多个,请用大括号!1)关系表达式:a、表达式的数值只能为1(表示为真),或0(表示假)。如 9>8这个关系表达式是真的,所以9>8这个表达式的数值就是1。 如 7<6这个关系表达式是假的,所以7<6这个表达式的数值就是0 b、考试最容易错的:就是int x=1,y=0,z=2;x<y<z是真还是假?带入为1<0<2,从数学的角度出发肯定是错的,但是如果是C语言那么就是正确的!因为要1<0为假得到0,表达式就变成了0<2那么运算结果就是1,称为了真的了!c、等号和赋值的区别!一定记住“=”就是赋值,“= =”才是等号。虽然很多人可以背诵,但我依然要大家一定好好记住,否则,做错了,我一定会强烈的鄙视你!2)逻辑表达式:核心:表达式的数值只能为1(表示为真),或0(表示假)a) 共有 && || ! 三种逻辑运算符号。b) !>&&>|| 优先的级别。c) 注意短路现象。考试比较喜欢考到。详细请见书上例子,一定要会做例1和例2d) 表示 x 小于0大于10的方法。0<x<10是不行的(一定记住)。是先计算0<x 得到的结果为1或则0;再用0,或1与10比较得到的总是真(为1)。所以一定要用 (0<x)&&(x<10)表示比0大比10小。3)if 语句a、else 是与最接近的if且没有else的语句匹配。b、交换的程序写法:t=x;x=y;y=t;c、if(a<b)t=a;a=b;b=t;if(a<b){t=a;a=b;b=t;}两个的区别,考试多次考到了!d、单独的if语句:if(a<b)t=a;标准的if语句:if(a<b)min=a; else min=b; 嵌套的if语句:if(a<b) if(b>c)printf(“ok!”); 多选一的if语句if(a= =t)printf(“a”); else if(b= =t)printf(“b”); else if(c= =t)printf(“c”); else pritnf(“d”); 通过习题,要熟悉以上几种if语句!经典考题:结合上面四种if语句题型做题,答错了,请自行了断!预备,开始!int a=1,b=0; if(!a)b++; else if(a= =0) if(a)b+=2; else b+=3;请问b的值是多少? 如果没有看懂题目,你千万不要自行了断,这样看得懂不会做的人才会有理由的活着。正确的是b为3。int a=1,b=0; if(!a)b++; 是假的不执行 else if(a= =0) 是假的执行 if(a)b+=2; 属于else if的嵌套if语句,不执行。 else b+=3; if-else-if语句没有一个正确的,就执行else的语句! 4)条件表达式:  表达式1 ?表达式2 :表达式3a、考试口诀:真前假后.b、注意是当表达式1的数值是非0时,才采用表达式2的数值做为整个运算结果,当表达式1的数值为0时,就用表达式3的数值做为整个的结果。c、int a=1,b=2,c=3,d=4,e=5;k=a>b?c:d>e?d:e;求k的数值时多少? 答案为san5)switch语句:a) 执行的流程一定要弄懂!上课时候详细的过程讲了,请自己一定弄懂!b)注意有break 和没有break的差别,书上的两个例子,没有break时候,只要有一个case匹配了,剩下的都要执行,有break则是直接跳出了swiche语句。break在C语言中就是分手,一刀两断的意思。c) switch只可以和break一起用,不可以和continue用。d) switch(x) x:是整型常量,字符型常量,枚举型数据。{case 1: …. 不可以是变量。 case 2: ….}e)switch是必考题型,请大家一定要完成书上的课后的switch的习题。第四章1)三种循环结构:a)for() ; while(); do- while()三种。 b)for循环当中必须是两个分号,千万不要忘记。 c)写程序的时候一定要注意,循环一定要有结束的条件,否则成了死循环。 d) do-while()循环的最后一个while();的分号一定不能够丢。(当心上机改错 do-while循环是至少执行一次循环。 1) break 和 continue的差别记忆方法:break:是打破的意思,(破了整个循环)所以看见break就退出整个一层循环。continue: 是继续的意思,(继续循环运算),但是要结束本次循环,就是循环体内剩下的语句不再执行,跳到循环开始,然后判断循环条件,进行新一轮的循环。3)嵌套循环就是有循环里面还有循环,这种比较复杂,要一层一层一步一步耐心的计算,一般记住两层是处理二维数组的。4)while((c=getchar())!=’\n’)和while(c=getchar() !=’\n’)的差别先看a = 3 != 2 和 (a=3)!=2 的区别:(!=号的级别高于=号 所以第一个先计算 3!=2) 第一个a的数值是得到的1;第二个a的数值是3。考试注意点: 括号在这里的重要性。5)每行输出五个的写法:for(i=0;i<=100;i++) printf(“%d”,i); if((i+1)%5==0) printf(“\n”); //如果i是从1开始的话,就是if(i%5==0)printf(“\n”); }6)如何整除一个数:i%5==0表示整除5I%2==0表示整除2,同时表示是偶数! 7)输入123,输出321逆序输出数据int i=123; while(i!=0) printf(“%d”,i%10); i=i/10; }8)for只管后面一个语句:inti=3; for(i=3;i<6;i++): printf(“#”): 请问最终打印几个#号?答案为一个!9)不停的输入,直到输入# 停止输入! 不停的输入,直到输入$停止输入!while( (x=getchar())!=’ # ’ ) while( (x=getchar())!=’$ ’ ) 不停的输入,直到遇到?停止输入! while((x=getchar())!=’ ? ’) 解说:一定要注意这种给出了条件,然后如何去写的方法! 10)for循环和switch语句的和在一起考题! 11)多次出现的考题:intk=1 int k=1; while(- -k); while(k--); printf(“%d”,k); printf(“%d”,k); 结果为0 结果为-1 第五章1、函数:是具有一定功能的一个程序块,是C语言的基本组成单位。2、函数不可以嵌套定义。但是可以嵌套调用。3、函数名缺省返回值类型,默认为 int。4、C语言由函数组成,但有且仅有一个main函数!是程序运行的开始!5、如何判断a是否为质数:背诵这个程序!void iszhishu( int a ) for(i=2;i<a/2;i++) if(a%i==0) printf(“不是质数”); printf(“是质数!”); }6、如何求阶层:n! 背诵这个程序!int fun(int n) int p=1; for(i=1;i<=n;i++) p=p*i; return p; }7、函数的参数可以是常量,变量,表达式,甚至是函数调用。add(int x,int y){returnx+y;} main() int sum; sum=add(add(7,8),9);请问sum的结果是多少? 结果为24 }8、 函数的参数,返回数值(示意图):9、一定要注意参数之间的传递实参和形参之间 传数值,和传地址的差别。(考试的重点)传数值的话,形参的变化不会改变实参的变化。传地址的话,形参的变化就会有可能改变实参的变化。10、函数声明的考查:一定要有:函数名,函数的返回类型,函数的参数类型。不一定要有:形参的名称。填空题也可能会考到!以下是终极难度的考题。打横线是函数声明怎么写!int*fun(int a[] , int b[]) ………….. }已经知道函数是这样。这个函数的正确的函数声明怎么写? int *fun(int *a , int *b) 这里是函数声明的写法,注意数组就是指针 int *fun(int a[] , int b[]) 这种写法也是正确的 int *fun(int b[] , int c[]) 这种写法也是正确的,参数的名称可以随便写 int *fun(int * , int *) 这种写法也是正确的,参数的名称可以不写 11、要求掌握的库函数:a、库函数是已经写好了函数,放在仓库中,我们只需要如何去使用就可以了!b、以下这些库函数经常考到,所以要背诵下来。abs()、 sqrt()、fabs()、pow()、sin() 其中pow(a,b)是重点。2^3是由pow(2,3)表示的。第六章指针一定要初始化NULL动态存储#include<malloc> int *p P=(int*)malloc(sizeof(int)*n) 指针变量的本质是用来放地址,而一般的变量是放数值的。1、 int p 中 p和p的差别:简单说*p是数值,p是地址!*p可以当做变量来用;*的作用是取后面地址p里面的数值 p是当作地址来使用。可以用在scanf函数中:scanf(“%d”,p); 2、p++ 和 (p)++的之间的差别:改错题目中很重要!考试超级重点*p++是 地址会变化。 口诀:取当前值,然后再移动地址! (*p)++ 是数值会要变化。 口诀:取当前值,然后再使数值增加1。 例题:int *p,a[]={1,3,5,7,9}; 请问*p++和(*p)++的数值分别为多少? *p++: 这个本身的数值为1。由于是地址会增加一,所以指针指向数值3了。(*p)++ 这个本身的数值为1。由于有个++表示数值会增加,指针不移动,但数值1由于自加了一次变成了2。 3、二级指针: *p:一级指针:存放变量的地址。 **q:二级指针:存放一级指针的地址。 常考题目: int x=7; int*p=&x,**q=p; 问你:*p为多少?*q为多少?**q为多少? 7 p 7 再问你:**q=&x的写法可以吗? 不可以,因为二级指针只能存放一级指针的地址。 4、三名主义:(考试的重点)数组名:表示第一个元素的地址。数组名不可以自加,他是地址常量名。(考了很多次)函数名:表示该函数的入口地址。字符串常量名:表示第一个字符的地址。5、移动指针(经常加入到考试中其他题目综合考试)char *s=“meikanshu” while(*s){printf(“%c”,*s);s++;} 这个s首先会指向第一个字母m然后通过循环会一次打印出一个字符,s++是地址移动,打印了一个字母后,就会移动到下一个字母!6、指针变量两种初始化(一定要看懂)方法一:int a=2,*p=&a;(定义的同时初始化) 方法二:int a=2,*p;  (定义之后初始化) p=&a; 7、传数值和传地址(每年必考好多题目)void fun(int a,intb) void fun(int *a,int *b) { int t ; { int t ; t=a;a=b;b=t; t=*a;*a=*b;*b=t; } } main() main() { int x=1,y=3, { int x=1,y=3, fun(x,y); fun(&x,&y) printf(“%d,%d”,x,y); intf(“%d,%d”,x,y); } } 这个题目答案是1和3。 这个题目的答案就是3和1。 传数值,fun是用变量接受,所以fun中 传地址,fun用指针接受!这个时候fun 的交换不会影响到main中的x和y 。 中的交换,就会影响到main中的x和y。 传数值,形参的变化不会影响实参。 传地址形参的变化绝大多数会影响到实参! 8、函数返回值是地址,一定注意这个*号(上机考试重点)int *fun(int*a,int *b)//可以发现函数前面有个*,这个就说明函数运算结果是地址 if(*a>*b) return a; //return a 可以知道返回的是a地址。 return b; main() { int x=7,y=8,*max; max = fun(&x,&y);//由于fun(&x,&y)的运算结果是地址,所以用max来接收。 printf(“%d,%d”,) } 9、考试重要的话语:指针变量是存放地址的。并且指向哪个就等价哪个,所有出现*p的地方都可以用它等价的代替。例如:int a=2,*p=&a; *p=*p+2; (由于p指向变量a,所以指向哪个就等价哪个,这里p等价于a,可以相当于是a=a+2) 第七章数组: 存放的类型是一致的。多个数组元素的地址是连续的。1、一维数组的初始化:inta[5]={1,2,3,4,5}; 合法 inta[5]={1,2,3, }; 合法 inta[]={1,2,3,4,5}; 合法,常考,后面决定前面的大小! inta[5]={1,2,3,4,5,6};不合法,赋值的个数多余数组的个数了 2、一维数组的定义;int a[5];注意这个地方有一个重要考点,定义时数组的个数不是变量一定是常量。 int a[5] 合法,最正常的数组 int a[1+1] 合法,个数是常量2,是个算术表达式 int a[1/2+4] 合法,同样是算术表达式 int x=5,int a[x]; 不合法,因为个数是x,是个变量,非法的, define P 5 int a[P] 合法,define 后的的P是符号常量,只是长得像变量 3、二维数组的初始化inta[2][3]={1,2,3,4,5,6}; 合法,很标准的二维的赋值。 inta[2][3]={1,2,3,4,5, }; 合法,后面一个默认为0。 inta[2][3]={{1,2,3,} {4,5,6}}; 合法,每行三个。 inta[2][3]={{1,2,}{3,4,5}}; 合法,第一行最后一个默认为0。 inta[2][3]={1,2,3,4,5,6,7}; 不合法,赋值的个数多余数组的个数了。 int a[2][]={1,2,3,4,5,6}; 不合法,不可以缺省列的个数。 int a[][3]={1,2,3,4,5,6}; 合法,可以缺省行的个数。 补充:1.一维数组的重要概念:对a[10]这个数组的讨论。1、a表示数组名,是第一个元素的地址,也就是元素a[0]的地址。(等价于&a) 2、a是地址常量,所以只要出现a++,或者是a=a+2赋值的都是错误的。 3、a是一维数组名,所以它是列指针,也就是说a+1是跳一列。  对a3的讨论。1、a表示数组名,是第一个元素的地址,也就是元素a[0][0]的地址。 2、a是地址常量,所以只要出现a++,或者是a=a+2赋值的都是错误的。 3、a是二维数组名,所以它是行指针,也就是说a+1是跳一行。 4、a[0]、a[1]、a[2]也都是地址常量,不可以对它进行赋值操作,同时它们都是列指针,a[0]+1,a[1]+1,a[2]+1都是跳一列。 5、注意a和a[0] 、a[1]、a[2]是不同的,它们的基类型是不同的。前者是一行元素,后三者是一列元素。 2.二维数组做题目的技巧:如果有a3={1,2,3,4,5,6,7,8,9}这样的题目。步骤一:把他们写成: 第一列 第二列 第三列  a[0]à  1    2   3 ->第一行 a[1]à 4   5   6  —>第二行 a[2]à 7   8   9  ->第三行 步骤二:这样作题目间很简单:    *(a[0]+1)我们就知道是第一行的第一个元素往后面跳一列,那么这里就是a[0][1]元素,所以是1。 *(a[1]+2)我们就知道是第二行的第一个元素往后面跳二列。那么这里就是a[1][2]元素,所以是6。 一定记住:只要是二维数组的题目,一定是写成如上的格式,再去做题目,这样会比较简单。3.数组的初始化,一维和二维的,一维可以不写,二维第二个一定要写int a[]={1,2} 合法。 int a[][4]={2,3,4}不合法。 但int a[4][]={2,3,4}合法。 4.二维数组中的行指针int a1; 其中a现在就是一个行指针,a+1跳一行数组元素。 搭配(*)p[2]指针a[0],a[1]现在就是一个列指针。a[0]+1 跳一个数组元素。搭配*p[2]指针数组使用5.还有记住脱衣服法则:超级无敌重要a[2] 变成 *(a+2) a[2][3]变成 *(a+2)[3] 再可以变成 *(*(a+2)+3) 这个思想很重要!其它考试重点文件的复习方法:把上课时候讲的文件这一章的题目要做一遍,一定要做,基本上考试的都会在练习当中。1.字符串的 strlen() 和 strcat() 和strcmp() 和strcpy()的使用方法一定要记住。他们的参数都是地址。其中strcat()和strcmp()有两个参数。2.strlen 和 sizeof的区别也是考试的重点;3.define f(x)(xx) 和 define f(x) xx 之间的差别。一定要好好的注意这写容易错的地方,替换的时候有括号和没有括号是很大的区别。int *p; p= (int *)malloc(4); p= (int *)malloc(sizeof(int));以上两个等价 当心填空题目,malloc的返回类型是 void *5.函数的递归调用一定要记得有结束的条件,并且要会算简单的递归题目。要会作递归的题目6.结构体和共用体以及链表要掌握最简单的。typedef考的很多,而且一定要知道如何引用结构体中的各个变量,链表中如何填加和删除节点,以及何如构成一个简单的链表,一定记住链表中的节点是有两个域,一个放数值,一个放指针。 内存计算 结构体 共用体 Int (4)char(1)double(8) 总内存 16 (最大内存为单位,存不下再开辟一个单元) 8(最大内存为单位,不停迭代) 结构体成员:结构体变量.成员 (*p).成员 p->成员名 成员为数组时输入不用&其他都要只能对最低一级成员操作 strcut 结构体{成员类型成员名}变量名列表 可以嵌套使用 链表及动态链表 在成员中加个指针 7.函数指针的用法(*f)()记住一个例子:int add(int x, int y) {....} main() int (*f)(); f=add; }赋值之后:合法的调用形式为1、add(2,3); 2、f(2,3); 3、(*f)(2,3) 8.两种重要的数组长度:char a[]={‘a’,’b’,’c’};  数组长度为3,字符串长度不定。sizeof(a)为3。 char a[5]={ ‘a’,’b’,’c’}  数组长度为5,字符串长度3。sizeof(a)为5。 9.scanf 和 gets的数据:如果输入的是 good good study! 那么scanf(“%s”,a); 只会接收 good. 考点:不可以接收空格。 gets(a); 会接收 good good study! 考点:可以接收空格。 10.共用体的考查:union TT int a; charch[2]; }考点一: sizeof (struct TT) = 4;12.指针迷惑的考点:char ch[]=”iamhandsome”; char *p=ch; 问你 *(p+2) 和 *p+2的结果是多少? ‘m’ ‘k’ 结果是这两个,想不通的同学请作死的想!想通为止! 13.数组中放数组一定要看懂: int a[8]={1,2,3,4,4,3,2,2}; int b[5]={0}; b[a[3]]++ 这个写法要看懂,结果要知道是什么?b[4]++,本身是0,运行完后,b[4]为1了。 14.字符串的赋值 C语言中没有字符串变量,所以用数组和指针存放字符串:1、char ch[10]={“abcdefgh”}; 对 2、char ch[10]=“abcdefgh”; 对 3、char ch[10]={‘a’,’b’,’c’,’d’,’e’,’f’,’g’,’h’}; 对 4、char *p=“abcdefgh”; 对 5、char *p; 对 p=“abcdefgh”; 6、char ch[10]; 错了!数组名不可以赋值! ch=“abcdefgh”; 7、char *p={“abcdefgh”}; 错了!不能够出现大括号! 15.字符串赋值的函数背诵:一定要背诵,当心笔试填空题目。把s指针中的字符串复制到t指针中的方法1、while( (*t=*s)!=null){s++;t++;} 完整版本 2、while( *t=*s ){s++;t++;} 简单版本 3、while( *t++=*s++); 高级版本 16.typedef 是取别名,不会产生新的类型,他同时也是关键字考点一:typedef int qq 那么 int x 就可以写成 qq x 考点二:typedef int *qq 那么 int *x就可以写成 qq x 17.static 考点是一定会考的!复习相关的习题。static int x;默认值为0。 int x:默认值为不定值。 18.函数的递归调用一定会考!至少是2分。关注微信公众号:[<font=red>果果小师弟],获取更多精彩内容!

51单片机原理以及接口技术(四)--80C51的程序设计

@TOC单片机应用系统是合理的硬件与完善的软件的有机组合。软件就是各种指令依某种规律组合形成的程序。程序设计(或软件设计)的任务是利用计算机语言对系统预完成的任务进行描述和规定。80C51单片机的程序设计主要采用两种语言,一种是汇编语言,另一种是高级语言(如C51)。采用高级语言进行程序设计,对系统硬件资源的分配比用汇编语言简单,且程序的阅读和修改比较容易,适于编写较大一点的程序。汇编语言生成的目标程序占存储空间少、运行速度快,具有效率高、实时性强的优点,适于编写短小高效的程序。由于汇编语言是面向机器的语言,对单片机的硬件资源操作直接方便、概念清晰,尽管对编程人员的硬件知识要求较高,但对于学习和掌握单片机的硬件结构极为有利。所以,这里我们仅对汇编语言进行介绍。点击此处访问小编的个人小站4.1 程序编制的方法和技巧4.1.1 程序编制的步骤  一、预完成任务的分析  首先,要对单片机应用系统预完成的任务进行深入的分析,明确系统的设计任务、功能要求和技术指标。其次,要对系统的硬件资源和工作环境进行分析。这是单片机应用系统程序设计的基础和条件。  二、进行算法的优化  算法是解决具体问题的方法。一个应用系统经过分析、研究和明确规定后, 对应实现的功能和技术指标可以利用严密的数学方法或数学模型来描述,从而把一个实际问题转化成由计算机进行处理的问题。同一个问题的算法可以有多种,结果也可能不尽相同,所以,应对各种算法进行分析比较,并进行合理的优化。比如,用迭代法解微分方程,需要考虑收敛速度的快慢(即在一定的时间里能否达到精度要求)。而有的问题则受内存容量的限制而对时间要求并不苛刻。对于后一种情况,速度不快但节省内存的算法则应是首选。  三、程序总体设计及流程图绘制  经过任务分析、算法优化后,就可以进行程序的总体构思,确定程序的结构和数据形式,并考虑资源的分配和参数的计算等。然后根据程序运行的过程, 勾画出程序执行的逻辑顺序,用图形符号将总体设计思路及程序流向绘制在平面图上,从而使程序的结构关系直观明了,便于检查和修改。通常,应用程序依功能可以分为若干部分,通过流程图可以将具有一定功能的各部分有机地联系起来,并由此抓住程序的基本线索,对全局可以有一个完整的了解。清晰正确的流程图是编制正确无误的应用程序的基础和条件,所以,绘制一个好的流程图,是程序设计的一项重要内容。流程图可以分为总流程图和局部流程图。总流程图侧重反映程序的逻辑结构和各程序模块之间的相互关系。局部流程图反映程序模块的具体实施细节。对于简单的应用程序,可以不画流程图。但当程序较为复杂时,绘制流程图是一个良好的编程习惯。  常用的流程图符号有开始和结束符号、工作任务符号、判断分支符号、程序连接符号、程序流向符号等,如图 4.1 所示。  此外,还应编制资源分配表,包括数据结构和形式、参数计算、通信协议、各子程序的入口和出口说明等。4.1.2 编制程序的方法和技巧  一、采用模块化程序设计方法单片机应用系统的程序一般由包含多个模块的主程序和各种子程序组成。每一程序模块都要完成一个明确的任务,实现某个具体的功能,如发送、接收、延时、打印、显示等。采用模块化的程序设计方法,就是将这些不同的具体功能程序进行独立的设计和分别调试,最后将这些模块程序装配成整体程序并进行联调。  模块化的程序设计方法具有明显的优点。把一个多功能的、复杂的程序划分为若干个简单的、功能单一的程序模块,有利于程序的设计和调试,有利于程序的优化和分工,提高了程序的阅读性和可靠性,使程序的结构层次一目了然。所以,进行程序设计的学习,首先要树立起模块化的程序设计思想。  二、尽量采用循环结构和子程序采用循环结构和子程序可以使程序的长度减少,占用内存空间减少。对于多重循环,要注意各重循环的初值和循环结束条件,避免出现程序无休止循环的“死循环”现象。对于通用的子程序,除了用于存放子程序入口参数的寄存器外,子程序中用到的其它寄存器的内容应压入堆栈进行现场保护,并要特别注意堆栈操作的压入和弹出的平衡。对于中断处理子程序除了要保护程序中用到的寄存器外,还应保护标志寄存器。这是由于在中断处理过程中难免对标志寄存器中的内容产生影响,而中断处理结束后返回主程序时可能会遇到以中断前的状态标志为依据的条件转移指令,如果标志位被破坏,则程序的运行就会发生混乱。4.1.3 汇编语言的语句格式  80C51 单片机汇编语言的语句行由 4 个字段组成,汇编程序能对这种格式正确地进行识别。这 4 个字段的格式为:  [标号:]操作码 [操作数] [;注释]  括号内的部分可以根据实际情况取舍。每个字段之间要用分隔符分隔,可以用作分隔符的符号有空格、冒号、逗号、分号等。如:  LOOP:MOV A,# 7FH ;A←7FH 一、标号  标号是语句地址的标志符号,用于引导对该语句的非顺序访问。有关标号的规定为:(1)标号由 1~8 个 ASCII 字符组成。第一个字符必须是字母,其余字符可以是字母、数字或其它特定字符;(2)不能使用该汇编语言已经定义了的符号作为标号。如指令助记符、寄存器符号名称等;(3)标号后边必须跟冒号。二、操作码  操作码用于规定语句执行的操作。它是汇编语句中惟一不能空缺的部分。它用指令助记符表示。三、操作数  操作数用于给指令的操作提供数据或地址。在一条汇编语句中操作数可能是空缺的,也可能包括一项,还可能包括两项或三项。各操作数间以逗号分隔。操作数字段的内容可能包括以下几种情况:(1)工作寄存器名;(2)特殊功能寄存器名;(3)标号名;(4)常数;(5)符号“$”,表示程序计数器 PC 的当前值;(6)表达式。四、注释  注释不属于汇编语句的功能部分,它只是对语句的说明。注释字段可以增加程序的可读性,有助于编程人员的阅读和维护。注释字段必须以分号“;”开头,长度不限,当一行书写不下时,可以换行接着书写,但换行时应注意在开头使用分号“;”。五、数据的表示形式  80C51 汇编语言的数据可以有以下几种表示形式:二进制数,末尾以字母 B 标识。如:1000 1111B;十进制数,末尾以字母 D 标识或将字母 D 省略。如:88D,66;十六进制数,末尾以字母 H 标识。如:78H,0A8H(应注意的是,十六进制数以字母 A~F 开头时应在其前面加上数字“0”);ASCII 码,以单引号括起来标识。如:‘AB’,‘1245’。4.2 源程序的编辑和汇编由于通用微型计算机的普及,现在单片机应用系统的程序设计都借助于通用微型计算机来完成。首先,在微机上利用各种编辑软件编写单片机的汇编语言源程序,然后使用交叉汇编程序对源程序进行汇编,并将获得的目标程序经仿真器或通用编程器写到单片机或程序存储器中,进而完成应用程序的调试。4.2.1 源程序的编辑与汇编一、源程序的编辑  源程序的编写要依据 80C51 汇编语言的基本规则,特别要用好常用的汇编命令(即伪指令)。例如下面的程序段:  ORG    0040H   MOV  A, #7FH  MOV  R1, #44H   END  这里的 ORG 和 END 是两条伪指令,其作用是告诉汇编程序此汇编源程序的起止位置。编辑好的源程序应以“ . ASM ”扩展名存盘,以备汇编程序调用。二、源程序的汇编  将汇编语言源程序转换为单片机能执行的机器码形式的目标程序的过程叫汇编。汇编常用的方法有两种,一是手工汇编,二是机器汇编。  手工汇编时,把程序用助记符指令写出后,通过手工方式查指令编码表, 逐个把助记符指令翻译成机器码,然后把得到的机器码程序(以十六进制形式) 键入到单片机开发机中,并进行调试。由于手工汇编是按绝对地址进行定位的, 所以,对于偏移量的计算和程序的修改非常不便。通常只在程序较小或开发条件有限制时才使用。  机器汇编是在常用的个人计算机 PC 上,使用交叉汇编程序将汇编语言源程序转换为机器码形式的目标程序。此时汇编工作由计算机完成,生成的目标程序由 PC 机传送到开发机上,经调试无误后,再固化到单片机的程序存储器ROM 中。机器汇编与手工汇编相比具有极大的优势,所以是汇编工作的首选。  源程序经过机器汇编后,形成的若干文件中含有两个主要文件,一个是列表文件,另一个是目标码文件。因汇编软件的不同,文件的格式及信息会有一些不同,但主要信息如下:  列表文件主要信息为:  地 址  目标码  汇编程序           ORG  0040H  0040H  747F  MOV  A,#7FH  0042H  7944  MOV  R1,#44H            END  目标码文件主要信息为:  首地址  末地址  目标码  0040H  0044H  747F7944  该目标码文件由 PC 机的串行口传送到开发机后,接下来的任务就是仿真调试了。4.2.2 伪指令我举一个例子就是,我们在读报告的时候有时候会在页脚写上“下转第三页”,就是跳过第二页直接看第三页,但是我们并不把“下转第三页”这五个字读出来,那么“转第三页”这五个字就相当于伪指令。怎么样是不是通俗易懂!!!  伪指令是汇编程序能够识别并对汇编过程进行某种控制的汇编命令。它不是单片机执行的指令,所以没有对应的可执行目标码,汇编后产生的目标程序中不会再出现伪指令。标准的 80C51 汇编程序定义了许多伪指令,下面仅对一些常用的进行介绍。一、起始地址设定伪指令 ORG  格式为:  ORG 表达式  该指令的功能是向汇编程序说明下面紧接的程序段或数据段存放的起始地址。表达式通常为 16 进制地址,也可以是已定义的标号地址。如:      ORG  8000H   START:MOV  A,#30H  此时规定该段程序的机器码从地址 8000H 单元开始存放。  在每一个汇编语言源程序的开始,都要设置一条 ORG 伪指令来指定该程序在存储器中存放的起始位置。若省略 ORG 伪指令,则该程序段从 0000H 单元开始存放。在一个源程序中,可以多次使用 ORG 伪指令规定不同程序段或数据段存放的起始地址,但要求地址值由小到大依序排列,不允许空间重叠。二、汇编结束伪指令 END  格式为: END  该指令的功能是结束汇编。  汇编程序遇到 END 伪指令后即结束汇编。处于 END 之后的程序,汇编程序将不处理。三、字节数据定义伪指令 DB  格式为: [标号:] DB 字节数据表  功能是从标号指定的地址单元开始,在程序存储器(ROM)中定义字节数据。  字节数据表可以是一个或多个字节数据、字符串或表达式。该伪指令将字节数据表中的数据根据从左到右的顺序依次存放在指定的存储单元中,一个数据占一个存储单元。例如:  DB “how are you?”  把字符串中的字符以 ASCII 码的形式存放在连续的 ROM 单元中。又如:  DB –2,–4,–6,8,10,18  把 6 个数转换为十六进制表示(FEH,FCH,FAH,08H,0AH,12H),并连续地存放在 6 个 ROM 单元中。  该伪指令常用于存放数据表格。如要存放显示用的十六进制的字形码,可以用多条 DB 指令完成:  DB 0C0H,0F9H,0A4H,0B0H   DB 99H,92H,82H,0F8H  DB 80H,90H,88H,83H  DB 0C6H,0A1H,86H,84H四、字数据定义伪指令 DW  格式为: [标号:] DW 字数据表  功能是从标号指定的地址单元开始,在程序存储器中定义字数据。  该伪指令将字或字表中的数据根据从左到右的顺序依次存放在指定的存储单元中。应特别注意:16 位的二进制数,高 8 位存放在低地址单元,低 8 位存放在高地址单元。  例如:        ORG 1400H   DATA:  DW 24AH,3CH  汇编后,(1400H)=32H,(1401H)= 4AH,(1402H)=00H,(1403H)=3CH。五、空间定义伪指令 DS  格式为:[标号:] DS 表达式  功能是从标号指定的地址单元开始,在程序存储器中保留由表达式所指定的个数的存储单元作为备用空间,并都填以零值。  例如:    ORG 3000HBUF: DS   50  汇编后,从地址 3000H 开始保留 50 个存储单元作为备用单元。六、赋值伪指令 EQU  格式为:符号名 EQU 表达式  功能是将表达式的值或特定的某个汇编符号定义为一个指定的符号名。例如: LEN EQU 10 SUM EQU 21H BLOCK EQU 22H CLR A MOV R7,#LEN MOV R0,#BLOCK LOOP:ADD A,@R0 INC R0 DJNZ R7,LOOP MOV SUM,A END  该程序的功能是,将 BLOCK 单元开始存放的 10 个无符号数进行求和,并将结果存入 SUM 单元中。七、位地址符号定义伪指令 BIT  格式为: 符号名 BIT 位地址表达式  功能是将位地址赋给指定的符号名。其中,位地址表达式可以是绝对地址, 也可以是符号地址。  例如:    ST BIT P1.0    将P1.0 的位地址赋给符号名 ST,在其后的编程中就可以用ST 来代替P1.0。4.3 基本程序结构4.3.1 顺序程序  顺序程序是指无分支、无循环结构的程序。其执行流程是依指令在存储器中的存放顺序进行的。一、数据传送  例 内部 RAM 的 2AH~2EH 单元中存储的数据如图 4.2 所示。试编写程序实现图 4.3 所示的数据传送结果。方法一:    MOV A,2EH ;2 字节,1 个机器周期    MOV 2EH,2DH ;3 字节,2 个机器周期    MOV 2DH,2CH ;3 字节,2 个机器周期    MOV 2CH,2BH ;3 字节,2 个机器周期    MOV 2BH,#00H ;3 字节,2 个机器周期方法二:    CLR A ;1 字节,1 个机器周期    XCH A,2BH ;2 字节,1 个机器周期    XCH A,2CH ;2 字节,1 个机器周期    XCH A,2DH ;2 字节,1 个机器周期    XCH A,2EH ;2 字节,1 个机器周期  以上两种方法均可以实现所要求的传送任务。方法一使用 14 个字节的指令代码,执行时间为 9 个机器周期;方法二仅用了 9 个字节的代码,执行时间也减少到了 5 个机器周期。实际应用中应尽量采用指令代码字节数少、执行时间短的高效率程序,即注意程序的优化。二、查表程序  例 有一变量存放在片内 RAM 的 20H 单元,其取值范围为:00H~05H。要求编制一段程序,根据变量值求其平方值,并存入片内 RAM 的 21H 单元。程序如下:  在程序存储器的一片存储单元中建立起该变量的平方表。用数据指针 DPTR 指向平方表的首址,则变量与数据指针之和的地址单元中的内容就是变量的平方值。程序流程图如图 4.4 所示。  采样 MOVC A,@A+PC 指令也可以实现查表功能,且不破坏 DPTR 的内容,从而可以减少保护 DPTR 的内容所需的开销。但表格只能存放在 MOVC A,@A+PC 指令后的 256 字节内,即表格存放的地点和空间有一定的限制。三、简单运算  由于 80C51 指令系统中只有单字节加法指令,因此对于多字节的相加运算必须从低位字节开始分字节进行。除最低字节可以使用 ADD 指令外,其它字节相加时要把低字节的进位考虑进去,这时就应该使用 ADDC 指令。  例 双字节无符号数加法。  设被加数存放在内部 RAM 的 51H、50H 单元,加数存放在内部 RAM 的61H、60H 单元,相加的结果存放在内部 RAM 的 51H、50H 单元,进位存放在位寻址区的 00H 位中。实现此功能的程序段如下: MOV R0, #50H ;被加数的低字节地址 MOV R1, #60H ;加数的低字节地址 MOV A, @R0 ;取被加数低字节 ADD A, @R1 ;加上加数低字节 MOV @R0, A ;保存低字节相加结果 INC R0 ;指向被加数高字节 INC R1 ;指向加数高字节 MOV A, @R0 ;取被加数高字节 ADDC A, @R1 ;加上加数高字节(带进位加) MOV @R0, A ;存高字节相加结果 MOV 00H, C ;保存进位4.3.2 分支程序  通常情况下,程序的执行是按照指令在程序存储器中存放的顺序进行的, 但根据实际需要也可以改变程序的执行顺序,这种程序结构就属于分支结构。分支结构可以分成单分支、双分支和多分支几种情况。  单分支结构如图 4.5 所示。若条件成立,则执行程序段 A,然后继续执行该指令下面的指令;如条件不成立,则不执行程序段 A,直接执行该指令的下条指令。  双分支结构如图 4.6 所示。若条件成立,执行程序段 A;否则执行程序段 B。  多分支结构如图 4.7 所示。先将分支按序号排列,然后按照序号的值来实现多分支选择。分支程序在单片机系统中有较多的应用,具体的实现上存在着许多技巧, 可以通过阅读一些典型的程序逐渐增加这方面的知识。一、单分支程序  例 求双字节补码。  设在内部 RAM 的 addr1 和 addr+1 单元存有一个双字节数(高位字节存于高地址单元)。编写程序将其读出取补后再存入 addr2 和 addr2+1 单元。  首先对低字节取补,然后判其结果是否为全“0”。若是,则高字节取补,否则高字节取反。程序段如下:START: MOV R0,#addr1 ;原码低字节地址送 R0 MOV R1,#addr2 ;补码低字节地址送 R1 MOV A, @R0 ;原码低字节送 A CPL A ;A 内容取补 INC A MOV @R1,A ;存补码低字节 INC R0 ;调整地址,指向下一单元 INC R1 JZ ZERO ;(A)=0 时转 ZERO MOV A,@R0 ;原码高字节送 A CPL A MOV @R1,A ;高字节反码存入 addr2+1 单元 SJMP LOOP1 ZERO: MOV A,@R0 ;高字节取补存入 addr2+1 单元 CPL A INC A MOV @R1,A LOOP1:RET 二、双分支程序  例设变量 x 以补码的形式存放在片内 RAM 的 30H 单元,变量 y 与 x 的关系是:当 x > 0 时,y = x;当 x = 0 时,y = 20H;当 x < 0 时,y = x + 5。编制程序,根据 x 的大小求 y 并送回原单元。程序段如下: START: MOV A,30H JZ NEXT ANL A,#80H ;判断符号位 JZ LP MOV A,#05H ADD A,30H MOV 30H,A SJMP LP NEXT: MOV 30H,#20H LP: SJMP $三、多分支程序  例根据 R7 的内容转向相应的处理程序。  设R7 的内容为 0~N,对应的处理程序的入口地址分别为 PP0~PPN。程序段如下: START: MOV DPTR,#TAB ;置分支入口地址表首址 MOV A,R7 ;分支转移序号送 A ADD A,R7 ;分支转移序号乘以 2 MOV R3,A ;暂存于 R3 MOVC A,@A+DPTR XCH A,R3 ;取高位地址 INC A MOVC A,@A+DPTR ;取低位地址 MOV DPL,A ;处理程序入口地址低 8 位送 DPL MOV DPH,R3 ;处理程序入口地址高 8 位送 DPL CLR A JMP @A+DPTR TAB: DW PP0 DW PP1 DW PPN4.3.3 循环程序  在程序设计中,经常需要控制一部分指令重复执行若干次,以便用简短的程序完成大量的处理任务。这种按某种控制规律重复执行的程序称为循环程序。循环程序有先执行后判断和先判断后执行两种基本结构,如图 4.8 所示。  图 4.8(a)为“先执行后判断”的循环程序结构图。其特点是一进入循环, 先执行循环处理部分,然后根据循环控制条件判断是否结束循环。若不结束, 则继续执行循环操作;若结束,则进行结束处理并退出循环。  图 4.8(b)为“先判断后执行”的循环程序结构图。其特点是将循环的控制部分放在循环的入口处,先根据循环控制条件判断是否结束循环。若不结束, 则继续执行循环操作;若结束,则进行结束处理并退出循环。一、先执行后判断  例 50ms 延时程序。  若晶振频率为 12MHz,则一个机器周期为 1μs。执行一条 DJNZ 指令需要2 个机器周期,即 2μs。采用循环计数法实现延时,循环次数可以通过计算获得,并选择先执行后判断的循环结构。程序段如下: DEL: MOV R7,#200 ;1 μs DEL1:MOV R6,#123 ;1 μs NOP ;1 μs DEL2:DJNZ R6,DEL2 ;2 μs,计(2×123)μs DJNZ R7,DEL1 ;2 μs,计 [(2×123+2+2)×200+1] μs,约 50ms RET  例 无符号数排序程序。在片内 RAM 中,起始地址为 30H 的 8 个单元中存放有 8 个无符号数。试对这些无符号数进行升序排序。  数据排序常用的方法是冒泡排序法。这种方法的过程类似水中气泡上浮, 故称冒泡法。执行时从前向后进行相邻数的比较,如数据的大小次序与要求的顺序不符就将这两个数互换,否则不互换。对于升序排序,通过这种相邻数的互换,使小数向前移动,大数向后移动。从前向后进行一次冒泡(相邻数的互换),就会把最大的数换到最后。再进行一次冒泡,就会把次大的数排在倒数第二的位置。  设 R7 为比较次数计数器,初始值为 07H,位地址 00H 为数据互换标志位。程序段如下: START: CLR 00H ;互换标志清 0 MOV R7,#07H ;各次冒泡比较次数 MOV R0,#30H ;数据区首址 LOOP: MOV A,@R0 ;取前数 MOV 2BH,A ;暂存 INC R0 MOV 2AH,@R0 ;取后数 CLR C SUBB A,@R0 ;前数减后数 JC NEXT MOV @R0,2BH ;前数小于后数,不互换 DEC R0 MOV @R0,2AH ;两数交换 INC R0 ;准备下一次比较 SETB 00H ;置互换标志 NEXT: DJNZ R7,LOOP ;进行下一次比较 JB 00H,START ;进行下一轮冒泡 SJMP $ 二、先判断后执行  例将内部 RAM 中起始地址为 data 的数据串传送到外部 RAM 中起始地址为 buffer 的存储区域内,直到发现‘$ ’字符停止传送。由于循环次数事先不知道,但循环条件可以测试到,所以,采用先判断后执行的结构比较适宜。程序段如下: MOV R0,#data MOV DPTR,#buffer LOOP0: MOV A,@R0 CJNE A,#24H,LOOP1 ;判断是否为‘ $ ’字符 SJMP LOOP2 ;是‘ $ ’字符,转结束 LOOP1: MOVX @DPTR,A ;不是‘ $ ’字符,执行传送 INC R0 INC DPTR SJMP LOOP0 ;传送下一数据 LOOP2:4.3.4 子程序及其调用  一、子程序的调用  在实际应用中,经常会遇到一些带有通用性的问题,如数值转换、数值计算等,在一个程序中可能要使用多次。这时可以将其设计成通用的子程序供随时调用。利用子程序可以使程序结构更加紧凑,使程序的阅读和调试更加方便。  子程序的结构与一般的程序并无多大区别,它的主要特点是,在执行过程中需要由其它程序来调用,执行完后又需要把执行流程返回到调用该子程序的主程序。  子程序调用时要注意两点,一是现场的保护和恢复,二是主程序与子程序的参数传递。  二、现场保护与恢复  在子程序执行过程中常常要用到单片机的一些通用单元,如工作寄存器R0~R7、累加器 A、数据指针 DPTR 以及有关标志和状态等。而这些单元中的内容在调用结束后的主程序中仍有用,所以需要进行保护,称为现场保护。在执行完子程序,返回继续执行主程序前恢复其原内容,称为现场恢复。保护与恢复的方法有以下两种:  1.在主程序中实现  其特点是结构灵活。示例如下: PUSH PSW ;保护现场 PUSH ACC ; PUSH B ; MOV PSW,#10H ;换当前工作寄存器组 LCALL addr16 ;子程序调用 POP B ;恢复现场 POP ACC ; POP PSW ;  2.在子程序中实现  其特点是程序规范、清晰。示例如下: SUB1: PUSH PSW ;保护现场 PUSH ACC ; PUSH B ; MOV PSW,#10H ;换当前工作寄存器组 POP B ;恢复现场 POP ACC ; POP PSW ; RET  应注意的是,无论哪种方法保护与恢复的顺序都要对应,否则程序将会发生错误。  三、参数传递  由于子程序是主程序的一部分,所以,在程序的执行时必然要发生数据上的联系。在调用子程序时,主程序应通过某种方式把有关参数(即子程序的入口参数)传给子程序。当子程序执行完毕后,又需要通过某种方式把有关参数(即子程序的出口参数)传给主程序。在 80C51 单片机中,传递参数的方法有三种。  1.利用累加器或寄存器在这种方式中,要把预传递的参数存放在累加器 A 或工作寄存器 R0~R7 中,即在主程序调用子程序时,应事先把子程序需要的数据送入累加器 A 或指定的工作寄存器中,当子程序执行时,可以从指定的单元中取得数据,执行运算。反之,子程序也可以用同样的方法把结果传送给主程序。  例 编写程序,实现 c=a2+b2。设 a、b、c 分别存于内部 RAM 的 30H、31H、32H 三个单元中。程序段如下: START:MOV A,30H ;取 a ACALL SQR ;调用查平方表 MOV R1,A ;a2 暂存于 R1 中 MOV A,31H ;取 b ACALL SQR ;调用查平方表 ADD A,R1 ;a2+b2 存于 A 中 MOV 32H,A ;存结果 SJMP $ SQR: MOV DPTR,#TAB ;子程序 MOVC A,@A+DPTR ; TAB: DB 0,1,4,9,16 ,25,36,49,64,81  2.利用存储器  当传送的数据量比较大时,可以利用存储器实现参数的传递。在这种方式中,事先要建立一个参数表,用指针指示参数表所在的位置。当参数表建立在内部 RAM 时,用 R0 或 R1 作参数表的指针。当参数表建立在外部 RAM 时, 用 DPTR 作参数表的指针。  例 将 R0 和 R1 指向的内部 RAM 中两个 3 字节无符号整数相加,结果送到由 R0 指向的内部 RAM 中。入口时,R0 和 R1 分别指向加数和被加数的低位字节;出口时,R0 指向结果的高位字节。低字节在高地址,高字节在低地址。程序段如下: NADD: MOV R7,#3 ;三字节加法 CLR C ; NADD1:MOV A,@R0 ;取加数低字节 ADDC A,@R1 ;被加数低字节加 MOV @R0, A ; DEC R0 DEC R1 DJNZ R7,NADD1 INC R0 RET  3.利用堆栈  利用堆栈传递参数是在子程序嵌套中常采用的一种方法。在调用子程序前,用 PUSH 指令将子程序中所需数据压入堆栈。进入执行子程序时,再用 POP 指令从堆栈中弹出数据。  例 把内部 RAM 中 20H 单元中的 1 个字节十六进制数转换为 2 位 ASCII码,存放在 R0 指示的两个单元中。程序段如下: MAIN: MOV A,20H ; SWAP A PUSH ACC ;参数入栈 ACALL HEASC POP ACC MOV @R0,A ;存高位十六进制数转换结果 INC R0 ;修改指针 PUSH 20H ;参数入栈 ACALL HEASC POP ACC MOV @R0,A ;存低位十六进制数转换结果 HEASC: MOV R1,SP ;借用 R1 为堆栈指针 DEC R1 DEC R1 ;R1 指向被转换数据 XCH A,@R1 ;取被转换数据 ANL A,#0FH ;取 1 位十六进制数 ADD A,#2 ;偏移量调整,所加值为 MOVC 与 DB间字节数 MOVC A,@A+PC ;查表 XCH A,@R1 ;1 字节指令,存结果于堆栈 RET ;1 字节指令 ASCTAB:DB 30H,31H,32H,33H,34H,35H,36H,37H DB 38H,39H,41H,42H,43H,44H,45H,46H  一般说来,当相互传递的数据较少时,采用寄存器传递方式可以获得较快的传递速度。当相互传递的数据较多时,宜采用存储器或堆栈方式传递。如果是子程序嵌套,最好采用堆栈方式。4. 4 常用程序举例4.4.1 算术运算程序  一般说来,单片机应用系统的任务就是对客观实际的各种物理参数进行测试和控制。所以,数据的运算是避免不了的。尽管数据运算并不是 80C51 单片机的优势所在,但运用一些编程技巧和方法,对于大部分测控应用中的运算,80C51 单片机还是能够胜任的。一、多字节数的加、减运算  80C51 单片机的指令系统提供的是字节运算指令,所以在处理多字节数的加减运算时,要合理地运用进位(借位)标志。  例 多字节无符号数的加法。  设两个 N 字节的无符号数分别存放在内部 RAM 中以 DATA1 和 DATA2 开始的单元中。相加后的结果要求存放在 DATA2 数据区。  程序段如下: MOV R0,#DATA1 ; MOV R1,#DATA2 ; MOV R7,#N ;置字节数 CLR C ; LOOP: MOV A,@R0 ; ADDC A,@R1 ;求和 MOV @R1,A ;存结果 INC R0 ;修改指针 INC R1 ; DJNZ R7,LOOP ;  二、多字节数乘法运算  例 双字节无符号数的乘法。  设双字节的无符号被乘数存放在 R3、R2 中,乘数存放在 R5、R4 中,R0 指向积的高位。算法及流程图如图 4.9 所示。  程序段如下: MULTB: MOV R7,#04 ;结果单元清 0 LOOP: MOV @R0,#00H ; DJNZ R7,LOOP ; ACALL BMUL ; SJMP $ BMUL: MOV A,R2 ; MOV B,R4 ; MUL AB ;低位乘 ACALL RADD ; MOV A,R2 ; MOV B,R5 ; MUL AB ;交叉乘 DEC R0 ; ACALL RADD ; MOV A,R4 ; MOV B,R3 ; MUL AB ;交叉乘 DEC R0 ; DEC R0 ; ACALL RADD ; MOV A,R5 ; MOV B,R3 ; MUL AB ;高字节乘 DEC R0 ; ACALL RADD ; DEC R0 RADD: ADD A,@R0 ; MOV @R0,A ; MOV A,B ; INC R0 ; ADDC A,@R0 ; MOV @R0,A ; INC R0 ; MOV A,@R0 ADDC A,#00H MOV @R0,A RET4.4.2 码型转换程序  单片机能识别和处理的是二进制码,而输入输出设备(如 LED 显示器、微型打印机等)则常使用 ASCII 码或 BCD 码。为此,在单片机应用系统中经常需要通过程序进行二进制码与 BCD 码或 ASCII 码的相互转换。  由于二进制数与十六进制数有直接的对应关系,所以,为了书写和叙述方便,下面将用十六进制数代替二进制数。  一、十六进制数与 ASCII 码间的转换  十六进制数与 ASCII 码的对应关系如表 4.1 所示。由表可见,当十六进制数在 0~9 之间时,其对应的 ASCII 码值为该十六进制数加 30H;当十六进制数在 A~F 之间时,其对应的 ASCII 码值为该十六进制数加 37H。  例 将 1 位十六进制数(即 4 位二进制数)转换成相应的 ASCII 码。  设十六进制数存放在 R0 中,转换后的 ASCII 码存放于 R2 中。实现程序如下:HASC: MOV A, R0 ;取 4 位二进制数 ANL A, #0FH ;屏蔽掉高 4 位 PUSH ACC ;4 位二进制数入栈 CLR C ;清进(借)位位 SUBB A,#0AH ;用借位位的状态判断该数在 0~9 还是A~F 之间 POP ACC ;弹出原 4 位二进制数 JC LOOP ;借位位为 1,跳转至 LOOP ADD A,#07H ;借位位为 0,该数在 A~F 之间,加 37H LOOP: ADD A,#30H ;该数在 0~9 之间,加 30H MOV R2, A ;ASCII 码存于R2 RET  例 将多位十六进制数转换成 ASCII 码。  设地址指针 R0 指向十六进制数低位,R2 中存放字节数,转换后地址指针R0 指向 ASCII 码的高位。R1 指向要存放的 ASCII 码的低位地址。实现程序如下: HTASC: MOV A,@R0 ;取低 4 位二进制数 ANL A,#0FH ; ADD A,#15 ;偏移量修正 MOVC A,@A+PC ;查表 MOV @R1,A ;存 ASCII 码 INC R1 ; MOV A ,@R0 ;取十六进制高 4 位 SWAP A ANL A,#0FH ; ADD A,#06H ;偏移值修正 MOVC A,@A+PC ; MOV @R1,A INC R0 ;指向下一单元 INC R1 ; DJNZ R2,HTASC ;ASCII 码存于 R2 RET ASCTAB:DB 30H,31H,32H,33H,34H,35H,36H,37H DB 38H,39H,41H,42H,43H,44H,45H,46H  二、BCD 码与二进制数之间的转换  在计算机中,十进制数要用 BCD 码来表示。通常,用 4 位二进制数表示一位 BCD 码,用 1 个字节表示 2 位 BCD 码(称为压缩型 BCD 码)。  例 双字节二进制数转换成 BCD 码。  设(R2R3)为双字节二进制数,(R4R5R6)为转换完的压缩型 BCD 码。  实现程序如下: DCDTH: CLR A ; MOV R4,A ;R4 清 0 MOV R5,A ;R5 清 0 MOV R6,A ;R6 清 0 MOV R7,#16 ;计数初值 LOOP: CLR C ; MOV A,R3 ; RLC A ; MOV R3,A ;R3 左移一位并送回 MOV A,R2 ; RLC A ; MOV R2,A ;R2 左移一位并送回 MOV A,R6 ; ADDC A,R6 ; DA A ; MOV R6,A ;(R6)乘 2 并调整后送回 MOV A,R5 ; ADDC A,R5 ; DA A ; MOV R5,A ;(R5)乘 2 并调整后送回 MOV A,R4 ; ADDC A,R4 ; DA A ; MOV R4,A ;(R4)乘 2 并调整后送回 DJNZ R7,LOOP ;本 章 小 结  汇编语言的源程序结构紧凑、灵活,汇编成的目标程序效率高,具有占存储空间少、运行速度快、实时性强等优点。但因它是面向机器的语言,所以它缺乏通用性,编程复杂繁琐,但应用相当广泛。  在进行程序设计时,首先需要对单片机应用系统预完成的任务进行深入的分析,明确系统的设计任务、功能要求、技术指标。然后,要对系统的硬件资源和工作环境进行分析和熟悉。经过分析、研究和明确规定后,利用数学方法或数学模型来对其进行描述,从而把一个实际问题转化成由计算机进行处理的问题。进而,对各种算法进行分析比较,并进行合理的优化。  模块化的程序设计方法具有明显的优点,所以,进行程序设计的学习,开始时就应该建立起模块化的设计思想。采用循环结构和子程序可以使程序的容量大大减少,提高程序的效率,节省内存。  80C51 汇编语言的语句行由 4 个字段组成,汇编程序能对这种格式正确地识别。伪指令是汇编程序能够识别的汇编命令,它不是单片机执行的指令,没有对应的机器码,仅用来对汇编过程进行某种控制。  汇编语言程序设计是实践性较强的一种单片机应用技能,它需要较多的编程训练和实际应用经验的积累。本章仅列出了一些最为基本的程序段示例以供参考。思考题及习题80C51 单片机汇编语言有何特点?利用 80C51 单片机汇编语言进行程序设计的步骤如何?常用的程序结构有哪几种?特点如何?子程序调用时,参数的传递方法有哪几种?什么是伪指令?常用的伪指令功能如何?设被加数存放在内部 RAM 的 20H、21H 单元,加数存放在 22H、23H 单元,若要求和存放在 24H、 25H 中,试编写出 16 位数相加的程序。编写一段程序,把外部 RAM 中 1000H~1030H 单元的内容传送到内部 RAM 的30H~60H 单元中。编写程序,实现双字节无符号数加法运算,要求 (R1R0)+(R7R6)→(61H60H)。若 80C51 的晶振频率为 6MHz,试计算延时子程序的延时时间。DELAY:MOV R7,#0F6HLP:MOV R6,#0FAH DJNZ R6,$ DJNZ R7,LP RET在内部 RAM 的 21H 单元开始存有一组单字节不带符号数,数据长度为 30H,要求找出最大数存入 IG 单元。编写程序,把累加器 A 中的二进制数变换成 3 位 BCD 码,并将百、十、个位数分别存放在内部 RAM 的 50H、51H、52H 单元中。编写子程序,将 R1 中的 2 个十六进制数转换为 ASCII 码后存放在 R3 和 R4 中。编写程序,求内部 RAM 中 50H~59H 十个单元内容的平均值,并存放在 5AH 单元。

51单片机原理以及接口技术(二)-单片机结构和原理

@TOCIntel公司推出的MCS-51系列单片机以其典型的结构、完善的总线、特殊功能寄存器的集中管理方式、位操作系统和面向控制的指令系统,为单片机的发展奠定了良好的基础。8051是MCS-51系列单片机的典型品种。众多单片机芯片生产厂商以8051为基核开发出的CHMOS工艺单片机产品统称为80C51系列。点击此处访问小编的个人小站,获取更多精彩内容2.1 80C51 系列概述2.1.1 MCS-51 系 列  MCS-51 是 Intel 公司生产的一个单片机系列名称。属于这一系列的单片机有多种,如 051/8751/8031,8052/8752/8032,80C51/87C51/80C31,80C52/87C52/80C32 等。  该系列单片机的生产工艺有两种:一是 HMOS工艺(即高密度短沟道 MOS工艺),二是 CHMOS 工艺(即互补金属氧化物的 HMOS 工艺)。CHMOS 是CMOS 和 HMOS 的结合,既保持了 HMOS 高速度和高密度的特点,还具有CMOS 低功耗的特点。在产品型号中凡带有字母“C”的,即为 CHMOS 芯片, 不带有字母“C”的,即为 HMOS 芯片。HMOS 芯片的电平与 TTL 电平兼容, 而 CHMOS 芯片的电平既与 TTL 电平兼容,又与 CMOS 电平兼容。所以,在单片机应用系统中应尽量采用 CHMOS 工艺的芯片。  在功能上,该系列单片机有基本型和增强型两大类,通常以芯片型号的末位数字来区分。末位数字为“1”的型号为基本型,末位数字为“2”的型号为增强型。如 8051/8751/8031、80C51/87C51/80C31 为基本型,而 8052/8752/8032、80C52/87C52/80C32 为增强型。  在片内程序存储器的配置上,该系列单片机有三种形式,即掩模 ROM、EPROM 和 ROMLes(s 无片内程序存储器)。如80C51 含有 4K 字节的掩模ROM, 87C51 含有 4K 字节的 EPROM,而 80C31 在芯片内无程序存储器,应用时要在单片机芯片外部扩展程序存储器。2.1.2 80C51 系 列  首先,80C51 是 MCS-51 系列单片机中 CHMOS 工艺的一个典型品种。另外,其它厂商以8051 为基核开发出的CHMOS 工艺单片机产品统称为80C51 系列。后面的叙述中若无特殊声明,“80C51”均指统称。当前常用的 80C51 系列单片机主要产品有:  Intel 公司的:80C31、80C51、87C51,80C32、80C52、87C52 等;  ATMEL 公司的:89C51、89C52、89C2051、89C4051 等。  除此之外,还有 Philips、华邦、Dallas、Siemens(Infineon)等公司的许多产品。  虽然这些产品在某些方面有一些差异,但基本结构和功能是相同的。所以, 以 80C51 代表这些产品的共性,而在具体的应用电路中,有时会采用某一产品的特定型号。2.2 80C51 的基本结构与应用模式2.2.1 80C51 的基本结构80C51 单片机的基本结构如图 2.1 所示。注:与并行口P3 复用的引脚:串行口输入和输出引脚RXD 和TXD;外部中断输入引脚 INT0和INT1 ;外部计数输入引脚 T0 和 T1;外部数据存储器写和读控制信号 WR 和RD 。由图可见,80C51 单片机主要由以下几部分组成:(1)CPU 系统8 位 CPU,含布尔处理器;时钟电路;总线控制逻辑。(2)存储器系统4 K 字节的程序存储器(ROM/EPROM/Flash,可外扩至 64 K);128 字节的数据存储器(RAM,可再外扩 64 K);特殊功能寄存器 SFR。(3)I/O 口和其它功能单元4 个并行 I/O 口;2 个 16 位定时/计数器;1 个全双工异步串行口;中断系统(5 个中断源、2 个优先级)。2.2.2 80C51 的应用模式一、总线型单片机应用模式通常的微处理器芯片都设有单独的地址总线、数据总线和控制总线。但单片机由于芯片引脚数量的限制,数据总线与地址总线经常采用复用方式,且许多引脚还要与并行 I/O 口引脚兼用。总线型单片机典型产品如 80C31/ AT89C51 等。  1.总线型应用的“三总线”模式  常用的总线型单片机有 40 个引脚,除电源、晶振输入引脚和仅能作通用并行 I/O 的 P1 口外,其余引脚大多是为应用系统总线扩展而设置的。利用这些引脚可以方便地将单片机配置成典型的“三总线”结构,如图 2.2 所示。  这时应用系统在以下方面可以得到扩展:芯片内部没有程序存储器(如 80C31)或芯片内程序存储器容量不够用时(如 80C51);系统需要扩展并行总线外围器件(如扩展并行可编程接口 81C55 或ADC0809 等)。  这种总线型应用在扩展外围器件较多时接线复杂,系统可靠性会降低。所以系统设计时,应尽量减少扩展器件的数量。  2.非总线型应用的“多 I/O”模式  总线型单片机也可以采用非总线型的“多 I/O”应用模式,这时单片机的扩展功能都不使用,因此可以利用的通用 I/O 口线的数量较多,如图 2.3 所示。由图可见,该模式极适于具有大量 I/O 口线需求的应用系统。二、非总线型单片机应用模式  非总线型单片机已经将用于外部总线扩展的 I/O 接口线和控制功能线去掉,从而使单片机的引脚数减少,体积减小。对于不需进行并行外围扩展、装置的体积要求苛刻且程序量不大的系统极其适合。非总线型单片机典型产品如AT89C2051/AT89C4051。2.3 80C51 典型产品资源配置与引脚封装2.3.1 80C51 典型产品资源配置  80C51 系列单片机基本组成虽然相同,但不同型号的产品在某些方面仍会有一些差异。典型的单片机产品资源配置如表 2.1 所示。  由表 2.1 可见:(1)增强型与基本型在以下几点有所不同:(我们一般用的都是STC89C52RC单片机,是增强型单片机)片内 ROM 从 4K 字节增加到 8K 字节;片内 RAM 从 128 字节增加到 256 字节;定时/计数器从 2 个增加到 3 个;中断源由 5 个增加到 6 个。(2)片内 ROM 的配置形式有以下几种:无 ROM(即 ROMLess)型,应用时要在片外扩展程序存储器;掩模 ROM(即 MaskROM)型,用户程序由芯片生产厂写入;EPROM 型,用户程序通过写入装置写入,通过紫外线照射擦除;FlashROM 型,用户程序可以电写入或擦除(当前常用方式)。  另外,有些单片机产品还提供 OTPROM 型(一次性编程写入 ROM)供应状态。通常OTPROM 型单片机较 Flash 型(属于 MTPROM,即多次编程 ROM) 单片机具有更高的环境适应性和可靠性,在环境条件较差时,应优先选择。2.3.2 80C51 的引脚封装  80C51 系列单片机采用双列直插式(DIP)、QFP44(Quad Flat Pack)和 LCC(Leaded Chip Carrier)形式引脚封装。这里仅介绍常用的总线型 DIP40 引脚封装和非总线型 DIP20 引脚封装,如图 2.4 所示。一、总线型 DIP40 引脚封装(1)电源及时钟引脚(4 个)VCC:电源接入引脚;VSS:接地引脚;XTAL1:晶体振荡器接入的一个引脚(采用外部振荡器时,此引脚接地);XTAL2:晶体振荡器接入的另一个引脚(采用外部振荡器时,此引脚作为外部振荡信号的输入端)。(2)控制线引脚(4 个)RST/VPD:复位信号输入引脚/备用电源输入引脚;ALE/PROG:地址锁存允许信号输出引脚/编程脉冲输入引脚;EA /VPP:内外存储器选择引脚/片内 EPROM(或 FlashROM)编程电压输入引脚;PSEN :外部程序存储器选通信号输出引脚。(3)并行 I/O 引脚(32 个,分成 4 个 8 位口)P0.0~P0.7:一般 I/O 口引脚或数据/低位地址总线复用引脚;P1.0~P1.7:一般 I/O 口引脚;P2.0~P2.7:一般 I/O 口引脚或高位地址总线引脚;P3.0~P3.7:一般 I/O 口引脚或第二功能引脚。二、非总线型 DIP20 封装引脚(以 89C2051 为例)(1)电源及时钟引脚(4 个)VCC:电源接入引脚;GND:接地引脚;XTAL1:晶体振荡器接入的一个引脚(采用外部振荡器时,此引脚接地);XTAL2:晶体振荡器接入的另一个引脚(采用外部振荡器时,此引脚作为外部振荡信号的输入端)。(2)控制线引脚(1 个)RST:复位信号输入引脚。(3)并行 I/O 引脚(15 个)P1.0~P1.7:一般 I/O 口引脚(P1.0 和 P1.1 兼作模拟信号输入引脚 AIN0 和AIN1);P3.0~P3.5、P3.7:一般 I/O 口引脚或第二功能引脚。2.4 80C51 的内部结构2.4.1 80C51 的内部结构  80C51 单片机由微处理器(含运算器和控制器)、存储器、I/O 接口以及特殊功能寄存器 SFR(图中用加黑方框和相应的标识符表示)等构成,如图 2.5所示。注:此图中未画出增强型单片机相关部件;加黑框内是可以寻址的 SFR(21 字节)。一、80C51 的微处理器  作为 80C51 单片机核心部分的微处理器是一个 8 位的高性能中央处理器(CPU)。它的作用是读入并分析每条指令,根据各指令的功能控制单片机的各功能部件执行指定的运算或操作。它主要由以下两部分构成:  1.运算器  运算器由算术/逻辑运算单元 ALU、累加器 ACC、寄存器 B、暂存寄存器、程序状态字寄存器 PSW组成。它所完成的任务是实现算术与逻辑运算、位变量处理和数据传送等操作。  ALU 功能极强,既可实现 8 位数据的加、减、乘、除算术运算和与、或、异或、循环、求补等逻辑运算,同时还具有一般微处理器所不具备的位处理功能。   累加器 ACC 用于向 ALU 提供操作数和存放运算的结果。在运算时将一个操作数经暂存器送至 ALU,与另一个来自暂存器的操作数在 ALU 中进行运算, 运算后的结果又送回累加器 ACC。同一般微机一样,80C51 单片机在结构上也是以累加器 ACC 为中心,大部分指令的执行都要通过累加器 ACC 进行。但为了提高实时性,80C51 的一些指令的操作可以不经过累加器 ACC,如内部 RAM 单元到寄存器的传送和一些逻辑操作。  寄存器 B 在乘、除运算时用来存放一个操作数,也用来存放运算后的一部分结果。在不进行乘、除运算时,可以作为通用的寄存器使用。  暂存寄存器用来暂时存放数据总线或其它寄存器送来的操作数。它作为ALU 的数据输入源,向 ALU 提供操作数。  程序状态字寄存器 PSW 是状态标志寄存器,它用来保存 ALU 运算结果的特征(如:结果是否为 0,是否有溢出等)和处理器状态。这些特征和状态可以作为控制程序转移的条件,供程序判别和查询。  2.控制器  同一般微处理器的控制器一样,80C51 的控制器也由指令寄存器 IR、指令译码器 ID、定时及控制逻辑电路和程序计数器 PC 等组成。  程序计数器 PC 是一个 16 位的计数器(注意:PC 不属于 SFR)。它总是存放着下一个要取指令的 16 位存储单元地址。也就是说,CPU 总是把 PC 的内容作为地址,从内存中取出指令码或含在指令中的操作数。因此,每当取完一个字节后,PC 的内容自动加 1,为取下一个字节做好准备。只有在执行转移、子程序调用指令和中断响应时例外,那时 PC 的内容不再加 1,而是由指令或中断响应过程自动给 PC 置入新的地址。单片机开机或复位时,PC 自动清 0,即装入地址 0000H,这就保证了单片机开机或复位后,程序从 0000H 地址开始执行。  指令寄存器 IR 保存当前正在执行的一条指令。执行一条指令,先要把它从程序存储器取到指令寄存器中。指令内容含操作码和地址码两部分,操作码送往指令译码器 ID,并形成相应指令的微操作信号。地址码送往操作数地址形成电路以便形成实际的操作数地址。  定时与控制是微处理器的核心部件,它的任务是控制取指令、执行指令、存取操作数或运算结果等操作,向其它部件发出各种微操作控制信号,协调各部件的工作。80C51 单片机片内设有振荡电路,只需外接石英晶体和频率微调电容就可产生内部时钟信号。二、80C51 的片内存储器  80C51 单片机的片内存储器与一般微机存储器的配置不同。一般微机的ROM 和 RAM 安排在同一空间的不同范围(称为普林斯顿结构)。而 80C51 单片机的存储器在物理上设计成程序存储器和数据存储器两个独立的空间(称为哈佛结构)。  基本型单片机片内程序存储器容量为 4 KB,地址范围是 0000H--0FFFH。增强型单片机片内程序存储器容量为 8 KB,地址范围是 0000H--1FFFH。  基本型单片机片内数据存储器均为 128 字节,地址范围是 00H--7FH,用于存放运算的中间结果、暂存数据和数据缓冲。这 128 字节的低 32 个单元用作工作寄存器,32 个单元分成 4 组,每组 8 个单元。在 20H~2FH 共 16 个单元是位寻址区,位地址的范围是 00H--7FH。然后是 80 个单元的通用数据缓冲区。  增强型单片机片内数据存储器为 256 字节,地址范围是 00H~FFH。低 128 字节的配置情况与基本型单片机相同。高 128 字节为一般 RAM,仅能采用寄存器间接寻址方式访问(而与该地址范围重叠的 SFR 空间采用直接寻址方式访问)。三、80C51 的 I/O 口及功能单元  80C51 单片机有 4 个 8 位的并行口,即 P0~P3。它们均为双向口,既可作为输入,又可作为输出。每个口各有 8 条 I/O 线。  80C51 单片机还有一个全双工的串行口(利用 P3 口的两个引脚 P3.0 和P3.1)。  80C51 单片机内部集成有 2 个 16 位的定时/计数器(增强型单片机有 3 个定时/计数器)。  80C51 单片机还具有一套完善的中断系统。四、80C51 的特殊功能寄存器(SFR)  80C51 单片机内部有 SP、DPTR(可分成 DPH、DPL 2 个 8 位寄存器)、PCON、IE、IP 等 21 个特殊功能寄存器单元,它们同内部 RAM 的 128 个字节统一编址,地址范围是 80H~FFH。这些 SFR 只用到了 80H~FFH 中的 21 个字节单元,且这些单元是离散分布的。  增强型单片机的 SFR 有 26 个字节单元,所增加的 5 个单元均与定时/计数器 2 相关。2.4.2 80C51 的时钟与时序  单片机的工作过程是:取一条指令、译码、进行微操作,再取一条指令、译码、进行微操作,这样自动地、一步一步地由微操作依序完成相应指令规定的功能。各指令的微操作在时间上有严格的次序,这种微操作的时间次序称作时序。单片机的时钟信号用来为单片机芯片内部的各种微操作提供时间基准。一、80C51 的时钟产生方式  80C51 单片机的时钟信号通常有两种产生方式:一是内部时钟方式,二是外部时钟方式。内部时钟方式如图 2.6(a)所示。在 80C51 单片机内部有一振荡电路,只要在单片机的 XTAL1 和 XTAL2 引脚外接石英晶体(简称晶振),就构成了自激振荡器并在单片机内部产生时钟脉冲信号。图中电容器 C1 和 C2 的作用是稳定频率和快速起振,电容值在 5--30 pF,典型值为 30 pF。晶振 CYS 的振荡频率范围为 1.2--12 MHz,典型值为 12 MHz 和 6 MHz。  外部时钟方式是把外部已有的时钟信号引入到单片机内,如图 2.6(b)所示。此方式常用于多片 80C51 单片机同时工作,以便于各单片机同步。一般要求外部信号高电平的持续时间大于 20 ns,且为频率低于 12 MHz 的方波。对于采用 CHMOS 工艺的单片机,外部时钟要由 XTAL1 端引入,而 XTAL2 端引脚应悬空。二、80C51 的时钟信号  晶振周期(或外部时钟信号周期)为最小的时序单位。如图 2.7 所示。  晶振信号经分频器后形成两相错开的时钟信号 P1 和 P2。时钟信号的周期也称为 S 状态,它是晶荡周期的两倍。即一个时钟周期包含 2 个晶振周期。在每个时钟周期的前半周期,相位 1(P1)信号有效,在每个时钟周期的后半周期,相位 2(P2)信号有效。每个时钟周期有两个节拍(相)P1 和 P2,CPU 以两相时钟 P1 和 P2 为基本节拍指挥各个部件谐调地工作。  晶振信号 12 分频后形成机器周期。一个机器周期包含 12 个晶荡周期或 6个时钟周期。因此,每个机器周期的 12 个振荡脉冲可以表示为 S1P1,S1P2,S2P1,S2P2,…,S6P2。  指令的执行时间称作指令周期。80C51 单片机的指令按执行时间可以分为三类:单周期指令、双周期指令和四周期指令(四周期指令只有乘、除两条指令)。  晶振周期、时钟周期、机器周期和指令周期均是单片机时序单位。晶振周期和机器周期是单片机内计算其它时间值(如波特率、定时器的定时时间等) 的基本时序单位。如晶振频率为 12MHz,则机器周期为 1 μs,指令周期为 1~ 4 μs。三、80C51 的典型时序  1.单周期指令时序  单字节指令时,时序如图 2.8(a)所示。在 S1P2 把指令操作码读入指令寄存器,并开始执行指令。但在 S4P2 读的下一指令的操作码要丢弃,且 PC 不加 1。  双字节指令时,时序如图 2.8(b)所示。在 S1P2 把指令操作码读入指令寄存器,并开始执行指令。在 S4P2 再读入指令的第二字节。  单字节指令和双字节指令均在 S6P2 结束操作。  2.双周期指令时序对于单字节指令,在两个机器周期之内要进行 4 次读操作。只是后 3 次读操作无效。如图 2.9 所示。由图中可以看到,每个机器周期中 ALE 信号有效两次,具有稳定的频率, 可以将其作为外部设备的时钟信号。  应注意的是,在对片外 RAM 进行读/写时,ALE 信号会出现非周期现象, 如图 2.10 所示。在第二机器周期无读操作码的操作,而是进行外部数据存储器的寻址和数据选通,所以在 S1P2~S2P1 间无 ALE 信号。2.4.3 80C51 的复位  复位是使单片机或系统中的其它部件处于某种确定的初始状态。单片机的工作就是从复位开始的。一、复位电路  当在 80C51 单片机的 RST 引脚引入高电平并保持 2 个机器周期时,单片机内部就执行复位操作(如果 RST 引脚持续保持高电平,单片机就处于循环复位状态)。   实际应用中,复位操作有两种基本形式:一种是上电复位,另一种是上电与按键均有效的复位。如图 2.11 所示。  上电复位要求接通电源后,单片机自动实现复位操作。常用的开机复位电路如图 2.11(a)所示。开机瞬间 RST 引脚获得高电平,随着电容 C1 的充电,RST 引脚的高电平将逐渐下降。RST 引脚的高电平只要能保持足够的时间(2 个机器周期),单片机就可以进行复位操作。该电路典型的电阻和电容参数为:晶振频率为 12 MHz 时,C1 为 10 μF,R1 为 8.2 kΩ;晶振频率为 6 MHz 时,C1为 22 μF,R1 为 1 kΩ。开机与按键均有效的复位电路如图 2.11(b)所示。开机复位原理与图 2.11(a)相同,另外,在单片机运行期间,还可以利用按键完成复位操作。晶振频率为 6MHz 时,R2 为 200 Ω。二、单片机复位后的状态  单片机的复位操作使单片机进入初始化状态。初始化后,程序计数器PC=0000H,所以程序从 0000H 地址单元开始执行。单片机启动后,片内 RAM为随机值,运行中的复位操作不改变片内 RAM 的内容。  特殊功能寄存器复位后的状态是确定的。P0~P3 为 FFH,SP 为 07H,SBUF 不定,IP、IE 和 PCON 的有效位为 0,其余的特殊功能寄存器的状态均为 00H。相应的意义为:P0~P3=FFH,相当于各接口锁存器已写入 1,此时不但可用于输出,也可以用于输入;SP=07H,堆栈指针指向片内 RAM 的 07H 单元(第一个入栈内容将写入 08H 单元);IP、IE 和 PCON 的有效位为 0,各中断源处于低优先级且均被关断,串行通信的波特率不加倍;PSW=00H,当前工作寄存器为 0 组。2.5 80C51 的存储器组织  存储器是组成计算机的主要部件,其功能是存储信息(程序和数据)。存储器可以分成两大类,一类是随机存取存储器(RAM),另一类是只读存储器(ROM)。  对于 RAM,CPU 在运行时能随时进行数据的写入和读出,但在关闭电源时,其所存储的信息将丢失。所以,它用来存放暂时性的输入输出数据、运算的中间结果或用作堆栈。  ROM 是一种写入信息后不易改写的存储器。断电后,ROM 中的信息保留不变。所以,ROM 用来存放固定的程序或数据,如系统监控程序、常数表格等。2.5.1 80C51 的程序存储器配置  80C51 单片机的程序计数器PC是16位的计数器,所以能寻址64KB的程序存储器地址范围,允许用户程序调用或转向 64KB 的任何存储单元。  MCS-51 系列的 80C51 在芯片内部有 4KB 的掩模 ROM,87C51 在芯片内部有 4KB 的 EPROM,而 80C31 在芯片内部没有程序存储器,应用时要在单片机外部配置一定容量的 EPROM。  80C51 的程序存储器配置如图 2.12 所示。2.5.2 80C51 的数据存储器配置  80C51 单片机的数据存储器分为片外 RAM 和片内 RAM 两大部分,如图2.13 所示。  80C51 片内 RAM 共有 128 字节,分成工作寄存器区、位寻址区、通用 RAM区三部分。  基本型单片机片内 RAM 地址范围是 00H~7FH。  增强型单片机(如 80C52)片内除地址范围在 00H~7FH 的 128 字节 RAM外,又增加了 80H--FFH 的高 128 字节的 RAM。增加的这一部分 RAM 仅能采用间接寻址方式访问(以与特殊功能寄存器 SFR 的访问相区别)。  片外 RAM 地址空间为 64KB,地址范围是 0000H~FFFFH。  与程序存储器地址空间不同的是,片外 RAM 地址空间与片内 RAM 地址空间在地址的低端 0000H~007FH 是重叠的。这就需要采用不同的寻址方式加以区分。访问片外 RAM 时使用专门的指令 MOVX,这时读( RD )或写( WR ) 信号有效;而访问片内 RAM 使用 MOV 指令,无读写信号产生。另外,与片内 RAM 不同,片外 RAM 不能进行堆栈操作。  在 80C51 单片机中,尽管片内 RAM 的容量不大,但它的功能多,使用灵活。一、工作寄存器区(采用寄存器寻址)  80C51 单片机片内 RAM 的低端 32 个字节分成 4 个工作寄存器组,每组占8 个单元。寄存器 0 组 :地址 00H~07H;寄存器 1 组 :地址 08H~0FH;寄存器 2 组 :地址 10H~17H;寄存器 3 组 :地址 18H~1FH。  每个工作寄存器组都有 8 个寄存器,分别称为 R0,R1,…,R7。程序运行时,只能有一个工作寄存器组作为当前工作寄存器组。  当前工作寄存器组的选择由特殊功能寄存器中的程序状态字寄存器 PSW 的RS1、RS0 来决定。可以对这两位进行编程,以选择不同的工作寄存器组。工作寄存器组与 RS1、RS0 的关系及地址如表 2.2 所示。  当前工作寄存器组从某一工作寄存器组换至另一工作寄存器组时,原来工作寄存器组的各寄存器的内容将被屏蔽保护起来。利用这一特性可以方便地完成快速现场保护任务。二、位寻址区  内部 RAM 的 20H 至 2FH 共 16 个字节是位寻址区。其 128 位的地址范围是00H~7FH。对被寻址的位可进行位操作。人们常将程序状态标志和位控制变量设在位寻址区内。该区未用的单元也可以作为通用 RAM 使用。位地址与字节地址的关系如表 2.3 所示。三、通用 RAM 区  位寻址区之后的 30H 至 7FH 共 80 个字节为通用 RAM 区。这些单元可以作为数据缓冲器使用。这一区域的操作指令非常丰富,数据处理方便灵活。  在实际应用中,常需在 RAM 区设置堆栈。80C51 的堆栈一般设在30H~7FH的范围内。栈顶的位置由 SP 寄存器指示。复位时 SP 的初值为 07H,在系统初始化时可以重新设置。2.5.3 80C51 的特殊功能寄存器  在 80C51 单片机中设置了与片内 RAM 统一编址的 21 个特殊功能寄存器(SFR),它们离散地分布在 80H~FFH 的地址空间中。字节地址能被 8 整除的(即十六进制的地址码尾数为 0 或 8 的)单元是具有位地址的寄存器。在 SFR 地址空间中,有效位地址共有 83 个,如表 2.4 所示。访问 SFR 只允许使用直接寻址方式。  特殊功能寄存器(SFR)每一位的定义和作用与单片机各部件直接相关。这里先概要介绍一下,详细用法在相应的章节中进行说明。一、与运算器相关的寄存器(3 个)累加器 ACC,8 位。它是 80C51 单片机中最繁忙的寄存器,用于向 ALU提供操作数,许多运算的结果也存放在累加器中;寄存器 B,8 位。主要用于乘、除法运算。也可以作为 RAM 的一个单元使用;程序状态字寄存器 PSW,8 位。其各位含义为CY:进位、借位标志。有进位、借位时 CY=1,否则 CY=0;AC:辅助进位、借位标志(高半字节与低半字节间的进位或借位);F0:用户标志位,由用户自己定义;RS1、RS0:当前工作寄存器组选择位;OV:溢出标志位。有溢出时 OV=1,否则 OV=0;P:奇偶标志位。存于 ACC 中的运算结果有奇数个 1 时 P=1,否则 P=0。二、指针类寄存器(3 个)SP 堆栈指针,8 位。它总是指向栈顶。80C51 单片机的堆栈常设在30H~7FH 这一段 RAM 中。堆栈操作遵循“后进先出”的原则,入栈操作时,SP 先加 1,数据再压入 SP 指向的单元。出栈操作时,先将 SP 指向单元的数据弹出,然后 SP 再减 1,这时 SP 指向的单元是新的栈顶。由此可见,80C51 单片机的堆栈区是向地址增大的方向生成的(这与常用的 80X86 微机不同);数据指针 DPTR,16 位。用来存放 16 位的地址。它由两个 8 位寄存器DPH 和 DPL 组成,可对片外 64 KB 范围的 RAM 或 ROM 数据进行间接寻址或变址寻址操作。三、与口相关的寄存器(7 个)并行 I/O 口 P0、P1、P2、P3,均为 8 位。通过对这 4 个寄存器的读/写操作,可以实现数据从相应口的输入/输出;串行口数据缓冲器 SBUF;串行口控制寄存器 SCON;串行通信波特率倍增寄存器 PCON(一些位还与电源控制相关,所以又称为电源控制寄存器)。四、与中断相关的寄存器(2 个)中断允许控制寄存器 IE;中断优先级控制寄存器 IP。五、与定时/计数器相关的寄存器(6 个)定时/计数器 T0 的两个 8 位计数初值寄存器 TH0、TL0,它们可以构成16 位的计数器,TH0 存放高 8 位,TL0 存放低 8 位;定时/计数器 T1 的两个 8 位计数初值寄存器 TH1、TL1,它们可以构成16 位的计数器,TH1 存放高 8 位,TL1 存放低 8 位;定时/计数器的工作方式寄存器 TMOD;定时/计数器的控制寄存器 TCON。2.6 80C51 的并行口结构与操作  80C51 单片机有 4 个 8 位的并行 I/O 口 P0、P1、P2 和 P3。各口均由口锁存器、输出驱动器和输入缓冲器组成。各口除可以作为字节输入/输出外,它们的每一条口线也可以单独地用作位输入/输出线。各口编址于特殊功能寄存器中,既有字节地址又有位地址。对口锁存器的读写,就可以实现口的输入/输出操作。  虽然各口的功能不同,且结构也存在一些差异,但每个口的位结构是相同的。所以,口结构的介绍均以其位结构进行说明。2.6.1 P0 口、P2 口的结构  当不需要外部程序存储器和数据存储器扩展时(如 80C51/87C51 的单片应用),P0 口、P2 口可用作通用的输入/输出口。  当需要外部程序存储器和数据存储器扩展时(如 80C31 的应用),P0 口作为分时复用的低 8 位地址/数据总线,P2 口作为高 8 位地址总线。一、P0 口的结构  P0 口由一个输出锁存器、一个转换开关 MUX、两个三态输入缓冲器、输出驱动电路和一个与门及一个反相器组成,如图 2.14 所示。图中控制信号 C 的状态决定转换开关的位置。当 C=0 时,开关处于图中所示位置;当 C=1 时,开关拨向反相器输出端位置。  1.P0 用作通用 I/O 口  当系统不进行片外 ROM 扩展(此时EA =1),也不进行片外 RAM 扩展(内部 RAM 传送使用“MOV”类指令)时,P0 用作通用 I/O 口。在这种情况下, 单片机硬件自动使控制 C=0,MUX 开关接向锁存器的反相输出端。另外,与门输出的“0”使输出驱动器的上拉场效应管 T1 处于截止状态。因此,输出驱动级工作在需外接上拉电阻的漏极开路方式。  作输出口时,CPU 执行口的输出指令,内部数据总线上的数据在“写锁存器”信号的作用下由 D 端进入锁存器,经锁存器的反相端送至场效应管 T2, 再经 T2 反相,在 P0.X 引脚出现的数据正好是内部总线的数据。  作输入口时,数据可以读自口的锁存器,也可以读自口的引脚。这要根据输入操作采用的是“读锁存器”指令还是“读引脚”指令来决定。  CPU 在执行“读—修改—写”类输入指令时(如:ANL P0,A),内部产生的“读锁存器”操作信号使锁存器 Q 端数据进入内部数据总线,在与累加器 A 进行逻辑运算之后,结果又送回 P0 的口锁存器并出现在引脚。读口锁存器可以避免因外部电路原因使原口引脚的状态发生变化造成的误读(例如,用一根口线驱动一个晶体管的基极,在晶体管的射极接地的情况下,当向口线写“1”时,晶体管导通,并把引脚的电平拉低到 0.7 V。这时若从引脚读数据,会把状态为 1 的数据误读为“0”。若从锁存器读,则不会读错)。  CPU 在执行“MOV”类输入指令时(如:MOV A,P0),内部产生的操作信号是“读引脚”。这时必须注意,在执行该类输入指令前要先把锁存器写入“1”,目的是使场效应管 T2 截止,从而使引脚处于悬浮状态,可以作为高阻抗输入。否则,在作为输入方式之前曾向锁存器输出过“0”,T2 导通会使引脚钳位在“0”电平,使输入高电平“1”无法读入。所以,P0 口在作为通用 I/O 口时,属于准双向口。  2.P0 用作地址/数据总线  当系统进行片外 ROM 扩展(此时EA =0)或进行片外 RAM 扩展(外部RAM 传送使用“MOVX @DPTR”类指令)时,P0 用作地址/数据总线。在这种情况下,单片机内硬件自动使 C=1,MUX 开关接向反相器的输出端,这时与门的输出由地址/数据线的状态决定。  CPU 在执行输出指令时,低 8 位地址信息和数据信息分时地出现在地址/ 数据总线上。若地址/数据总线的状态为“1”,则场效应管 T1 导通、T2 截止,引脚状态为“1”;若地址/数据总线的状态为“0”,则场效应管 T1 截止、T2 导通,引脚状态为“0”。可见 P0.X 引脚的状态正好与地址/数据线的信息相同。  CPU 在执行输入指令时,首先低 8 位地址信息出现在地址/数据总线上,P0.X 引脚的状态与地址/数据总线的地址信息相同。然后,CPU 自动地使转换开关 MUX 拨向锁存器,并向 P0 口写入 FFH,同时“读引脚”信号有效,数据经缓冲器进入内部数据总线。  由此可见,P0 口作为地址/数据总线使用时是一个真正的双向口。二、P2 口的结构  P2 口由一个输出锁存器、一个转换开关 MUX、两个三态输入缓冲器、输出驱动电路和一个反相器组成。图中控制信号 C 的状态决定转换开关的位置。当 C=0 时,开关处于图中所示位置;当 C=1 时,开关拨向地址线位置。P2 口的位结构如图 2.15 所示。由图可见,P2 口的输出驱动电路与 P0 口不同,其内部设有上拉电阻(由两个场效应管并联构成)。  1.P2 用作通用 I/O 口  当不需要在单片机芯片外部扩展程序存储器(对于 80C51/87C51,EA =1),只需扩展 256 字节的片外 RAM 时(访问片外 RAM 利用“MOVX @Ri”类指令来实现),只用到了地址线的低 8 位,P2 口不受该类指令的影响,仍可以作为通用 I/O 口使用。  CPU 在执行输出指令时,内部数据总线的数据在“写锁存器”信号的作用下由 D 端进入锁存器,经反相器反相后送至场效应管 T2,再经 T2 反相,在 P2.X引脚出现的数据正好是内部总线的数据。  P2 口用作输入时,数据可以读自口的锁存器,也可以读自口的引脚。这要根据输入操作采用的是“读锁存器”指令还是“读引脚”指令来决定。  CPU 在执行“读—修改—写”类输入指令时(如:ANL P2,A),内部产生的“读锁存器”操作信号使锁存器 Q 端数据进入内部数据总线,在与累加器 A 进行逻辑运算之后,结果又送回 P2 的口锁存器并出现在引脚。CPU 在执行“MOV”类输入指令时(如:MOV A,P2),内部产生的操作信号是“读引脚”。应在执行输入指令前把锁存器写入“1”,目的是使场效应管 T2 截止,从而使引脚处于高阻抗输入状态。所以,P2 口在作为通用 I/O 口时,属于准双向口。  2.P2 用作地址总线  当需要在单片机芯片外部扩展程序存储器( EA =0)或扩展的 RAM 容量超过 256 字节时(读/写片外 RAM 或 I/O 采用“MOVX @DPTR”类指令),单片机内硬件自动使控制 C=1,MUX 开关接向地址线,这时 P2.X 引脚的状态正好与地址线输出的信息相同。2.6.2 P1 口、P3 口的结构  P1 口是 80C51 惟一的单功能口,仅能用作通用的数据输入/输出口。  P3 口是双功能口,除具有数据输入/输出功能外,每一口线还具有特殊的第二功能。一、P1 口的结构P1 口的位结构如图 2.16 所示。  由图可见,P1 口由一个输出锁存器、两个三态输入缓冲器和输出驱动电路组成。其输出驱动电路与 P2 口相同,内部设有上拉电阻。  P1 口是通用的准双向 I/O 口。输出高电平时,能向外提供拉电流负载,不必再接上拉电阻。当口用作输入时,须向口锁存器写入 1。二、P3 口的结构  P3 口的位结构如图 2.17 所示。P3 口由一个输出锁存器、三个输入缓冲器(其中两个为三态)、输出驱动电路和一个与非门组成。其输出驱动电路与 P2口和 P1 口相同,内部设有上拉电阻。  1.P3 用作第一功能的通用 I/O 口当 CPU 对 P3 口进行字节或位寻址时(多数应用场合是把几条口线设为第二功能,另外几条口线设为第一功能,这时宜采用位寻址方式),单片机内部的硬件自动将第二功能输出线的 W 置 1。这时,对应的口线为通用 I/O 口方式。作为输出时,锁存器的状态(Q 端)与输出引脚的状态相同;作为输入时, 也要先向口锁存器写入 1,使引脚处于高阻输入状态。输入的数据在“读引脚” 信号的作用下,进入内部数据总线。所以,P3 口在作为通用 I/O 口时,也属于准双向口。  2.P3 用作第二功能使用  当 CPU 不对 P3 口进行字节或位寻址时,单片机内部硬件自动将口锁存器的 Q 端置 1。这时,P3 口可以作为第二功能使用。各引脚的定义如下:P3.0:RXD(串行口输入);P3.1:TXD(串行口输出);P3.2: INT0 (外部中断 0 输入);P3.3: INT1 (外部中断 1 输入);P3.4:T0(定时/计数器 0 的外部输入);P3.5:T1(定时/计数器 1 的外部输入);P3.6: WR (片外数据存储器“写”选通控制输出);P3.7: RD (片外数据存储器“读”选通控制输出)。P3 口相应的口线处于第二功能,应满足的条件是:(1)串行 I/O 口处于运行状态(RXD,TXD);(2)外部中断已经打开( INT0 、 INT1 );(3)定时器/计数器处于外部计数状态(T0、T1);(4)执行读/写外部 RAM 的指令( RD 、 WR )。  作为输出功能的口线(如 TXD),由于该位的锁存器已自动置 1,与非门对第二功能输出是畅通的,即引脚的状态与第二功能输出是相同的。  作为输入功能的口线(如 RXD),由于此时该位的锁存器和第二功能输出线均为 1,场效应管 T 截止,该口引脚处于高阻输入状态。引脚信号经输入缓冲器(非三态门)进入单片机内部的第二功能输入线。2.6.3 并行口的负载能力  P0、P1、P2、P3 口的输入和输出电平与 CMOS 电平和 TTL 电平均兼容。  P0 口的每一位口线可以驱动 8 个 LSTTL 负载。在作为通用 I/O 口时,由于输出驱动电路是开漏方式,由集电极开路(OC 门)电路或漏极开路电路驱动时需外接上拉电阻;当作为地址/数据总线使用时,接口线输出不是开漏的, 无须外接上拉电阻。  P1、P2、P3 口的每一位能驱动 4 个 LSTTL 负载。它们的输出驱动电路设有内部上拉电阻,所以可以方便地由集电极开路(OC 门)电路或漏极开路电路所驱动,而无须外接上拉电阻。  由于单片机口线仅能提供几毫安的电流,当作为输出驱动一般晶体管的基极时,应在口与晶体管的基极之间串接限流电阻。本 章 小 结  MCS-51 是 Intel 公司生产的一个单片机系列名称。其它厂商以 8051 为基核开发出的 CHMOS 工艺单片机产品统称为 80C51 系列。80C51 单片机在功能上分为基本型和增强型,在制造上采用 CHMOS 工艺。在片内程序存储器的配置上有掩模 ROM、EPROM 和 Flash、无片内程序存储器等形式。  80C51 单片机由微处理器、存储器、I/O 口以及特殊功能寄存器 SFR 构成。  80C51 单片机的时钟信号有内部时钟方式和外部时钟方式两种。内部的各种微操作都以晶振周期为时序基准。晶振信号二分频后形成两相错开的时钟信号 P1 和 P2,十二分频后形成机器周期。一个机器周期包含 12 个晶振周期(或6 个时钟周期)。指令的执行时间称作指令周期。  80C51 单片机的存储器在物理上设计成程序存储器和数据存储器两个独立的空间。片内程序存储器容量为 4 KB,片内数据存储器为 128 字节。  80C51 单片机有 4 个 8 位的并行 I/O 口:P0 口、P1 口、P2 口和 P3 口。各口均由接口锁存器、输出驱动器和输入缓冲器组成。P1 口是惟一的单功能口, 仅能用作通用的数据输入/输出口。P3 口是双功能口,除具有数据输入/输出功能外,每一条接口线还具有不同的第二功能,如 P3.0 是串行输入口线,P3.1 是串行输出口线。在需要外部程序存储器和数据存储器扩展时,P0 口作为分时复用的低 8 位地址/数据总线,P2 口作为高 8 位地址总线。  单片机的复位操作使单片机进入初始化状态。复位后,PC 内容为 0000H,P0 口~P3 口内容为 FFH,SP 内容为 07H,SBUF 内容不定,IP、IE 和 PCON的有效位为 0,其余的特殊功能寄存器的状态均为 00H。思考题及习题1.80C51 单片机在功能上、工艺上、程序存储器的配置上有哪些种类?2.80C51 单片机存储器的组织采用何种结构?存储器地址空间如何划分?各地址空间的地址范围和容量如何?在使用上有何特点?3.80C51 单片机的 P0~P3 口在结构上有何不同?在使用上有何特点?4.如果 80C51 单片机晶振频率为 12MHz,时钟周期、机器周期为多少?5.80C51 单片机复位后的状态如何?复位方法有几种?6.80C51 单片机的片内、片外存储器如何选择?7.80C51 单片机的 PSW 寄存器各位标志的意义如何?8.80C51 单片机的当前工作寄存器组如何选择?9.80C51 单片机的控制总线信号有哪些?各信号的作用如何?10.80C51 单片机的程序存储器低端的几个特殊单元的用途如何?关注微信公众号:[<font=red>果果小师弟],获取更多精彩内容!

画出属于你的最漂亮的数字时序图—WaveDrom

摘要:WaveDrom是一个免费开源的在线数字时序图渲染引擎。它可以使用JavaScript, HTML5和SVG来将时序图的WaveJSON描述转成SVG矢量图形,从而进行显示。WaveDrom可以嵌入到任何网页中。WaveDrom编辑器可在浏览器中运行,也可以安装在系统上,渲染引擎可以嵌入到任何网页中。一、WaveDrom功能绘制数字时序图、绘制寄存器图、绘制逻辑电路图二、WaveDrom的使用在线编辑器 https://wavedrom.com/editor.html官网 https://wavedrom.com/WaveDrom可以在线编辑也可以下载安装,可以在官网首页找到这两个入口。在线编辑方式,版本较新,但需要联网。下载安装方式,版本较官网旧一些,无需联网即可使用。在编辑器中输入WaveJSON 格式的数字时序图描述,即可实时渲染出相应的数字时序图。aveJSON 格式是十分简单的,主要需要记忆的是各个符号所对应的波形。三、绘制时序图下面的代码将创建名为“Alfa”的1位信号,该信号随时间改变其状态。{ "signal" : [{ "name": "Alfa", "wave": "01.zx=ud.23.456789" }] }“wave”字符串中的每个字符都代表一个时间段。符号“将以前的状态再延长一段时间。下面是它的外观:加时钟:数字时钟是一种特殊类型的信号。它在每个时间段内变化两次,可以有正负极性。它还可以在工作边缘上有一个可选标记。时钟块可以与其他信号状态混合,以产生时钟选通效应。代码如下:{ signal: [ { name: "pclk", wave: 'p.......' }, { name: "Pclk", wave: 'P.......' }, { name: "nclk", wave: 'n.......' }, { name: "Nclk", wave: 'N.......' }, { name: 'clk0', wave: 'phnlPHNL' }, { name: 'clk1', wave: 'xhlhLHl.' }, { name: 'clk2', wave: 'hpHplnLn' }, { name: 'clk3', wave: 'nhNhplPl' }, { name: 'clk4', wave: 'xlh.L.Hx' }, ]}以及呈现的图表:WaveLanes 可以统一在以数组形式表示的命名组中。['组名', {...}, {...}, ...]数组的第一个条目是组的名称,这些组可以嵌套。{signal: [ { name: 'clk', wave: 'p..Pp..P'}, ['Master', ['ctrl', {name: 'write', wave: '01.0....'}, {name: 'read', wave: '0...1..0'} { name: 'addr', wave: 'x3.x4..x', data: 'A1 A2'}, { name: 'wdata', wave: 'x3.x....', data: 'D1' }, ['Slave', ['ctrl', {name: 'ack', wave: 'x01x0.1x'}, { name: 'rdata', wave: 'x.....4x', data: 'Q2'}, ]}四、时序图教程网址:https://wavedrom.com/tutorial.html里面包含多个示例,可以很好地对WaveDrom进行学习。五、逻辑电路图教程网址:https://wavedrom.com/tutorial2.html里面讲解了逻辑电路图的绘制示例。六、Github主页WaveDrom Github https://github.com/wavedrom/wavedrom七、VScode中使用Waveform在VScode应用商店中搜索Waveform Render,这个就相当于WaveDrom左边键入代码,右边会自动生成时序图,非常好用:关注微信公众号:[果果小师弟],获取更多精彩内容!

嵌入式最强调试终端神器—MobaXterm

摘要:现今软件市场上有很多终端工具,比如:secureCRT、Putty等等。secureCRT其实也是一款很强大的终端工具,但它是收费软件,一般公司不允许使用。Putty,非常小巧,免费软件,但是不支持标签,开多个会话的话就需要开多个窗口,窗口切换不方便。这两个软件的界面都不太美观。今天介绍的是一款集万千于一身的全能型终端神器——MobaXterm!这款神器的优点:支持SSH,FTP,串口,VNC,X server等功能;支持标签,切换也十分方便;众多快捷键,操作方便;有丰富的插件可以免费安装;直接的便携版,不用安装。内建多标签和多终端分屏.....MobaXterm软件下载MobaXterm有免费版本和收费版本,对于普通的开发者来说免费版已经够用了,付费版支持很多定制的功能,专业人士才可能用得到。使用免安装的版本也很多方便,你可以把它拷贝的U盘上面,可以在不同的电脑使用而且设置不会丢失,在家里有公司所有配置都一样。下载或安装完成之后打开,界面如下:点击Session就可以新建不同的会话:下面简单介绍一下几个常用的协议:SSH:Secure Shell,较可靠、专为远程登录会话和其他网络服务提供安全性的协议。利用 SSH 协议可以有效防止远程管理过程中的信息泄露问题。最常见的我们可以用它来登录我们的Linux服务器。RDP:远程桌面协议(RDP)是一个多通道(multi-channel)的协议,可以连上提供微软终端机服务的计算机。FTP:文件传输协议(File Transfer Protocol)是用于在网络上进行文件传输的标准协议,可以在windows和liunx设备上互换文件。Serial:串口就不用多说了,可以打印串口设备的调试信息。SSH登录Linux主机点击Session,在弹出的对话框中选择SSH,后填入远程主机的IP地址,用户名,端口号,然后点击OK即可。如果的电脑上有Ubuntu虚拟机,那么这里的远程主机的IP地址就是你ubuntu的IP地址,用户名就是ubuntu主机的用户名,端口号默认为22。点击确定之后就进入到ubuntu主机了可以看到左边就是ubuntu主机的文件夹,右边就是终端窗口了。在里面输入linux命令,都是可以的。当然上传文件和下载文件也是很方便的,直接拖动或者右键也是支持的。RDP远程登录windows主机点击Session,在弹出的对话框中选择RDP,后填入远程主机的IP地址,用户名,端口号,然后点击OK即可。如果的电脑上有windows虚拟机,那么这里的远程主机的IP地址就是你虚拟机下windows的IP地址,用户名就是windows主机的用户名,端口号默认为3389。点击确定之后就进入到windows虚拟机了。就可以进行各种操作了,是不是很方便。FTP实现Linux和Windows文件互传在学习linux时候,会频繁的在Windows和 Ubuntu下进行文件传输,比如在Windwos下进行代码编写,然后将编写好的代码拿到Ubuntu下进行编译。Windows 和Ubuntu下的文件互传需要使用FTP服务,首先要在开启Ubuntu下的FTP服务, 然后在Windows安装FTP客户端,大多数使用的是FileZilla这个免费的FTP 客户端软件,这个软件很好用,但是我们的MobaXterm也是支持这个功能的。MobaXterm已经将这个FTP集成到这里面了,现在使用MobaXterm新建一个FTP连接。点击Session,在弹出的对话框中选择FTP,后填入ubuntu主机的IP地址,用户名,端口号,然后点击OK即可。这里的远程主机的IP地址就是你虚拟机下ubuntu的IP地址,用户名就是ubuntu主机的用户名,端口号默认为21。点击确定之后就可以互传文件了。和FileZilla操作方式一模一样,速度也很快。Serial作为调试终端使用MobaXterm的可以作为串口终端使用,当然你用串口调试助手也可以打印调试信息,但是不能终端使用,也就是不能输入命令。终端软件和串口调试助手不一样,终端软件功能更强大,在学习linux时使用串口调试助手等工具会出现命令不能输入,不能登陆。点击Session,在弹出的对话框中选择Serial,打开串口设置窗口以后先选择要设置的串口号,要先用串口线将开发板连接到电脑上,然后设置波特率,MobaXterm软件可以自动识别串口,因此我们直接下拉选择即可,波特率也是同样的设置方式。完了以后还要设置串口的其他功能。点击Advanced Serial settings选项卡,设置串口的其他功能,比如串口号、数据位、停止位、奇偶校验和硬件流控等,如果要设置终端相关的功能的话点击Terminal settings即可,比如终端字体以及字体大小等。设置完成以后点击下方的OK按钮即可。比如我们烧写U-boot时候,在倒计时3秒钟的时候按下回车键就可以输入u-boot对用的命令,打印出对应的信息了。5. 几个有用的设置隐藏菜单栏按钮菜单栏下的那排按钮感觉有点鸡肋,全部可以从菜单栏里找到,在菜单栏点击view -> show menu bar,即可隐藏此排按钮,去掉它可以省下很大空间。打开右键粘贴在Mobaxterm中右键粘贴功能默认不打开文本功能。可以手动打开。在菜单栏点击settings -> Configuration,在弹出的对话框中选择terminal,再将paste using right-click打上对勾即可。关闭自动弹出SFTPMoba在连接上远程电脑之后,将自动打开左侧的SFTP侧边栏。有时我们并不需要SFTP,因此可以将自动弹出SFTP功能关闭掉。在菜单栏点击settings > Configuration,在弹出的对话框中选择SSH,再将automaticall switch to SSH-browser tab after login前面的对勾去掉即可。MobaXterm中文乱码MobaXterm默认是UTF-8,若出现乱码可执行以下操作:在对应的终端点击Teminal settings -> Term charset即可。小结本文介绍了四种连接方式:SSH,RDP、FTP,Serial,以及四个有用的设置。当然Mobaxterm的功能远不止这些,但这四种连接方式是最基本,最常用的,需要各位在使用中慢慢摸索啦!关注微信公众号:[果果小师弟],获取更多精彩内容!

我在STM32单片机上跑神经网络算法—CUBE-AI

摘要:为什么可以在STM上面跑人工智能?简而言之就是通过X-Cube-AI扩展将当前比较热门的AI框架进行C代码的转化,以支持在嵌入式设备上使用,目前使用X-Cube-AI需要在STM32CubeMX版本7.0以上,目前支持转化的模型有Keras、TF lite、ONNX、Lasagne、Caffe、ConvNetJS。Cube-AI把模型转化为一堆数组,而后将这些数组内容解析成模型,和Tensorflow里的模型转数组后使用原理是一样的。一、环境安装和配置STM32CubeMXMDK/IAR/STM32CubeIDEF4/H7/MP157开发板二、AI神经网络模型搭建这里使用官方提供的模型进行测试,用keras框架训练:https://github.com/Shahnawax/HAR-CNN-Keras模型介绍在Keras中使用CNN进行人类活动识别:此存储库包含小型项目的代码。该项目的目的是创建一个简单的基于卷积神经网络(CNN)的人类活动识别(HAR)系统。该系统使用来自3D加速度计的传感器数据,并识别用户的活动,例如:前进或后退。HAR意为Human Activity Recognition (HAR) system,即人类行为识别。这个模型是根据人一段时间内的3D加速度数据,来判断人当前的行为,比如走路,跑步,上楼,下楼等,很符合Cortex-M系列MCU的应用场景。使用的数据如下图所示。存储库包含以下文件HAR.py,Python脚本文件,包含基于CNN的人类活动识别(HAR)模型的Keras实现,actitracker_raw.txt、包含此实验中使用的数据集的文本文件,model.h5,一个预训练模型,根据训练数据进行训练,evaluate_model.py、Python 脚本文件,其中包含评估脚本。此脚本在提供的 testData 上评估预训练 netowrk 的性能,testData.npy,Python 数据文件,包含用于评估可用预训练模型的测试数据,groundTruth.npy,Python 数据文件,包含测试数据的相应输出的地面真值和README.md.这么多文件不要慌,模型训练后得到model.h5模型,才是我们需要的。三、新建工程1.这里默认大家都已经安装好了STM32CubeMX软件。在STM32上验证神经网络模型(HAR人体活动识别),一般需要STM32F3/F4/L4/F7/L7系列高性能单片机,运行网络模型一般需要3MB以上的闪存空间,一般的单片机不支持这么大的空间,CUBEMX提供了一个压缩率的选项,可以选择合适的压缩率,实际是压缩神经网络模型的权重系数,使得网络模型可以在单片机上运行,压缩率为8,使得模型缩小到366KB,验证可以通过;然后按照下面的步骤安装好CUBE.AI的扩展包这个我安装了三个,安装最新版本的一个版本就可以。接下来就是熟悉得新建工程了因为安装了AI的包,所以在这个界面会出现artificial intelligence这个选项,点击Enable可以查看哪一些芯片支持AI接下来就是配置下载接口和外部晶振了。然后记得要选择一个串口作为调试信息打印输出。选择Software Packs,进入后把AI相关的两个包点开,第一个打上勾,第一个选择Validation。System Performance工程:整个应用程序项目运行在STM32MCU上,可以准确测量NN推理结果,CP∪U负载和内存使用情况。使用串行终端监控结果(e.g.Tera Term)Validation工程:完整的应用程序,在桌面PC和基于STM32 Arm Cortex-m的MCU嵌入式环境中,通过随机或用户测试数据,递增地验证NN返回的结果。与 X-CUBE-A验证工具一起使用。Application Template工程:允许构建应用程序的空模板项目,包括多网络支持。之后左边栏中的Software Packs点开,选择其中的X-CUBE-AI,弹出的Mode窗口中两个复选框都打勾,Configuration窗口中,点开network选项卡。选择刚刚配置的串口作为调试用。点击add network,选择上述下载好的model点h5模型,选择压缩倍数8;点击分析,可从中看到模型压缩前后的参数对比点击validation on desktop 在PC上进行模型验证,包括原模型与转换后模型的对比,下方也会现在验证的结果。致此,模型验证完成,下面开始模型部署四、模型转换与部署时钟配置,系统会自动进行时钟配置。按照你单片机的实际选型配置时钟就可以了。最后点击GENERATE CODE生成工程。然后在MDK中编译链接。选择好下载器后就可以下载代码了。然后打开串口调试助手就可以看到一系列的打印信息了。代码烧写在芯片里后,回到CubeMX中下图所示位置,我们点击Validate on target,在板上运行验证程序,效果如下图,可以工作,证明模型成功部署在MCU中。这次就这样先跑一下官方的例程,以后再研究一下,跑跑自己的模型。参考资料:https://youtube/grgNXdkmzzQ?t=10https://youtube/grgNXdkmzzQ?t=103关注微信公众号:[果果小师弟],获取更多精彩内容!智果芯—服务于百万大学生和电子工程师

推荐一种超简单的硬件位带bitband操作方法,让变量,寄存器控制,IO访问更便捷,无需用户计算位置

摘要:51 单片机中通过关键字 sbit来实现位定义,操作时除了被操作的那一位发生改变之外,其它位不受影响。不过在STM32里面就没有 sbit 关键字了,不能直接对寄存器的进行单个位操作,如果你想单独修改寄存器某一位的话,其实还是有办法的—位带操作。说明:M3,M4内核都支持硬件位带操作,M7内核不支持。一、硬件位带操作优势优势1比如我们在地址0x2000 0000定义了一个变量unit8_t a, 如果我们要将此变量的bit0清零,而其它bit不变。a & = ~0x01这个过程就需要读变量a,修改bit0,然后重新赋值给变量a,也就是读 - 修改 - 写经典三部曲,如果我们使用硬件位带就可以一步就完成,也就是所谓的原子操作,优势是不用担心中断或者RTOS任务打断。优势2操作便捷,适合用于需要频繁操作修改的场合,移植性强。不频繁的直接标准库或者HAL库配置即可。二、背景知识这个点知道不知道都没有关系,不影响我们使用硬件位带,可以直接看下面案例的操作方法,完全不需要用户去了解。位带操作就是对变量每个bit的操作,以M4内核的STM32F4为例:(1)将1MB地址范围0x20000000 - 0x200FFFFF映射到32MB空间范围0x22000000- 0x23FFFFFF ----> 这个对应STM32F4的通用RAM空间。也就是说1MB空间每个bit都拓展为32bit来访问控制下面这个图非常具有代表性。0x20000000地址的字节变量 bit0 映射到0x22000000来控制。 0x20000000地址的字节变量 bit1 映射到0x22000004来控制。 0x20000000地址的字节变量 bit2 映射到0x22000008来控制。 ..........依次类推(2)将1MB地址范围 0x40000000 - 0x400FFFFF 映射到32MB空间范围0x42000000 - 0x43FFFFFF ----> 这个对应STM32F4的外设空间。同样也是1MB空间每个bit都拓展为32bit来访问控制(3)举例,比如访问0x2000 0010地址里面字节变量的bit2那么实际要访问的就是:bit_word_addr = bit_band_base + (byte_offset x 32) + (bit_number × 4) 0x22000208 = 0x22000000 + (0x10*32) + (2*4)通过对地址空间0x22000208进行赋值为0x01就表示bit2置位,赋值为0x00就表示bit2清零,对这个地址空间读取操作就可以反应bit2的数值。三、超简单实现方案和四个经典案例这种硬件未带让用户去使用非常不方便,还需要倒腾地址计算。这里以MDK为例,提供一种IDE支持的,直接加后缀__attribute__((bitband))即可,对于M3和M4可以直接转换为硬件位带实现。案例1:超简单控制RAM空间变量定义:typedef struct { uint8_t bit0 : 1; uint8_t bit1 : 1; uint8_t bit2 : 1; uint8_t bit3 : 1; uint8_t bit4 : 1; uint8_t bit5 : 1; uint8_t bit6 : 1; uint8_t bit7 : 1; } TEST __attribute__((bitband)); TEST tTestVar;我们定义了一个8bit的变量tTestVar,控制每个bit的方法如下:tTestVar.bit0 = 1; tTestVar.bit1 = 1; tTestVar.bit2 = 1; tTestVar.bit3 = 0; tTestVar.bit4 = 0; tTestVar.bit5 = 1; tTestVar.bit6 = 1; tTestVar.bit7 = 1; 看汇编,已经修改为硬件位带:案例2:超简单控制GPIO输入输出寄存器GPIO里面最常用的就是输入输出。GPIO输出寄存器定义如下,每个bit控制一个IO引脚。我们软件定义如下:typedef struct { uint16_t ODR0 : 1; uint16_t ODR1 : 1; uint16_t ODR2 : 1; uint16_t ODR3 : 1; uint16_t ODR4 : 1; uint16_t ODR5 : 1; uint16_t ODR6 : 1; uint16_t ODR7 : 1; uint16_t ODR8 : 1; uint16_t ODR9 : 1; uint16_t ODR10 : 1; uint16_t ODR11 : 1; uint16_t ODR12 : 1; uint16_t ODR13 : 1; uint16_t ODR14 : 1; uint16_t ODR15 : 1; uint16_t Reserved : 16; } GPIO_ORD __attribute__((bitband)); GPIO_ORD *GPIOA_ODR = (GPIO_ORD *)(&GPIOA->ODR); GPIO_ORD *GPIOB_ODR = (GPIO_ORD *)(&GPIOB->ODR); GPIO_ORD *GPIOC_ODR = (GPIO_ORD *)(&GPIOC->ODR); GPIO_ORD *GPIOD_ODR = (GPIO_ORD *)(&GPIOD->ODR); GPIO_ORD *GPIOE_ODR = (GPIO_ORD *)(&GPIOE->ODR); GPIO_ORD *GPIOF_ODR = (GPIO_ORD *)(&GPIOF->ODR); GPIO_ORD *GPIOJ_ODR = (GPIO_ORD *)(&GPIOJ->ODR); GPIO_ORD *GPIOK_ODR = (GPIO_ORD *)(&GPIOK->ODR);GPIO输入寄存器定义如下:我们软件定义如下:typedef struct { uint16_t IDR0 : 1; uint16_t IDR1 : 1; uint16_t IDR2 : 1; uint16_t IDR3 : 1; uint16_t IDR4 : 1; uint16_t IDR5 : 1; uint16_t IDR6 : 1; uint16_t IDR7 : 1; uint16_t IDR8 : 1; uint16_t IDR9 : 1; uint16_t IDR10 : 1; uint16_t IDR11 : 1; uint16_t IDR12 : 1; uint16_t IDR13 : 1; uint16_t IDR14 : 1; uint16_t IDR15 : 1; uint16_t Reserved : 16; } GPIO_IDR __attribute__((bitband)); GPIO_IDR *GPIOA_IDR = (GPIO_IDR *)(&GPIOA->IDR); GPIO_IDR *GPIOB_IDR = (GPIO_IDR *)(&GPIOB->IDR); GPIO_IDR *GPIOC_IDR = (GPIO_IDR *)(&GPIOC->IDR); GPIO_IDR *GPIOD_IDR = (GPIO_IDR *)(&GPIOD->IDR); GPIO_IDR *GPIOE_IDR = (GPIO_IDR *)(&GPIOE->IDR); GPIO_IDR *GPIOF_IDR = (GPIO_IDR *)(&GPIOF->IDR); GPIO_IDR *GPIOJ_IDR = (GPIO_IDR *)(&GPIOJ->IDR); GPIO_IDR *GPIOK_IDR = (GPIO_IDR *)(&GPIOK->IDR);实际操作效果动态,注意看调试状态寄存器变化,控制GPIOA的PIN0到PIN3案例3:超方便的寄存器修改比如定时器TIM1的CR寄存器:我们的定义如下:typedef struct { uint16_t CEN : 1; uint16_t UDIS : 1; uint16_t URS : 1; uint16_t OPM : 1; uint16_t DIR : 1; uint16_t CMS : 2; uint16_t APRE : 1; uint16_t CKD : 2; uint16_t Reserved : 6; } TIM_CR1 __attribute__((bitband)); TIM_CR1 *TIM1_CR1 = (TIM_CR1 *)(&TIM1->CR1);实际操作动态效果,注意看调试状态寄存器变化,设置TIM1 CR1寄存器的每个bit控制:由于标准库,HAL库配置这些已经非常方便了,我们再使用这种方式意义不是很大,但对于需要频繁操作的地方,这种方式就非常好使了,言简意赅,移植性强,强力推荐,而且是原子操作方式,不用怕中断打断。案例4:应用进阶最后我们来个进阶,比如我们通过32位带宽的FMC总线扩展出来32个GPIO,如果我们采用如下使用方式就非常不直观#define HC574_PORT *(uint32_t *)0x64001000操作bit1 =0清零,就需要如下操作:HC574_PORT &= ~(1<<1);操作bit2和bit10置位,就需要如下操作:HC574_PORT |= ( ( 1<< 2) | (1<<10))这种操作会导致以后的代码修改非常不便,别人移植使用也非常不方便。如果我们改成如下方式,就方便太多了。typedef struct uint32_t tGPRS_TERM_ON : 1; uint32_t tGPRS_RESET :1; uint32_t tNRF24L01_CE :1; uint32_t tNRF905_TX_EN :1; uint32_t tNRF905_TRX_CE :1; uint32_t tNRF905_PWR_UP :1; uint32_t tESP8266_G0 :1; uint32_t tESP8266_G2 :1; uint32_t tLED1 :1; uint32_t tLED2 :1; uint32_t tLED3 :1; uint32_t tLED4 :1; uint32_t tTP_NRST :1; uint32_t tAD7606_OS0 :1; uint32_t tAD7606_OS1 :1; uint32_t tAD7606_OS2 :1; uint32_t tY50_0 :1; uint32_t tY50_1 :1; uint32_t tY50_2 :1; uint32_t tY50_3 :1; uint32_t tY50_4 :1; uint32_t tY50_5 :1; uint32_t tY50_6 :1; uint32_t tY50_7 :1; uint32_t tAD7606_RESET :1; uint32_t tAD7606_RANGE :1; uint32_t tY33_2 :1; uint32_t tY33_3 :1; uint32_t tY33_4 :1; uint32_t tY33_5 :1; uint32_t tY33_6 :1; uint32_t tY33_7 :1; }FMCIO_ODR __attribute__((bitband)); FMCIO_ODR *FMC_EXTIO = (FMCIO_ODR *)0x60001000;比如控制AD7606的OS0引脚高电平就是FMC_EXTIO->tAD7606_OS0 = 1;控制OS0引脚是低电平就是:FMC_EXTIO->tAD7606_OS0 = 0;简单易用,超方便。M7内核为什么不支持M内核权威指南作者Joseph Yiu回复:1、Cache问题,如果SRAM所在区域开启了读写Cache,使用位带操作的话,会有数据一致性问题。2、位带需要总线锁机制,在AHB总线协议中这相对容易实现,但在AXI总线协议中这有点混乱,并且在锁定序列期间,它可能导致其他总线主控的延迟更长。

STM32 ADC采样频率的理解

最大采样率如果设置PLCK2为6分频,那么ADCCLK为:72M/6=12MHz。在外部晶振为8MHZ的情况下,这是F103系列ADC得到的最大时钟频率。最小采样周期为1.5个周期+12.5周期=14周期。那么最大采样频率为:12MHZ/14周期=851.142KHZ≈851KHZ也就是1s可以采样851K个数据,对于STM32F1这个采样率已经是最大能力了。最小采样率如果设置PLCK2为8分频,那么ADCCLK为:72M/8=9MHz。在外部晶振为8MHZ的情况下,这是F103系列ADC得到的最小时钟频率。最大采样周期为239.5个周期+12.5周期=252周期。那么最大采样频率为:9MHZ/252周期=35.714KHZ≈35.7KHZ也就是1s可以采样35.7K个数据,对于STM32F1这个采样率时其最小的采样能力。

干货|教你使用Doxygen制作出漂亮程序文档

摘要:不知道大家有没有把自己的代码整理成文档的习惯,有没有给自己的代码一个非常漂亮的注释,就像下图这样。如果你写了一个结构体或者枚举是否也是这样注释的?如果每个人的注释都是这样写的话,被人怎么可能看不懂你的代码?如果你不是这样的话,你就必须要看这篇文章了。等等,别走!还有~你是不是也看过很多说明文档,比如下面这样的关于STM32标准外设驱动文档。你有没有想象过自己的代码也是可以这样打包成这样一个非常漂亮的文档的?今天就教大家如何给写注释,如何写出漂亮规范的注释,让人看着心旷神怡,透人心脾。如何写出规范的说明文档,让人看了直呼内行,给你点赞!Doxygen能将程序中的特定批注转换成为说明文件。它可以依据程序本身的结构,将程序中按规范注释的批注经过处理生成一个纯粹的参考手册,通过提取代码结构或借助自动生成的包含依赖图、继承图)以及协作图来可视化文档之间的关系,Doxygen生成的帮助文档的格式可以是CHM、RTF、PostScript、PDF、HTML等。微软出品的HTML Help WorkShop是制作CHM文件的最佳工具,它能将HTML文件编译生成CHM文档。Doxygen软件默认生成HTML文件或Latex文件,我们要通过HTML生成CHM文档,需要先安装HTML Help WorkShop软件,并在Doxygen中进行关联即可。下面将按照这张思维导图来讲述如何安装软件,以及如何写单片机注释和最终如何导出文档。1、windows安装doxygen首先在官网下载doxygen,网址:https://www.doxygen.nl/download.html下载完成后傻瓜式一步一步安装就可以了。安装完成后在开始栏点击Doxywizard就可以打开软件了。2、Linux安装doxygen这里我是使用的是ubuntu虚拟机,使用sudo apt-get install doxygen就可以安装了。安装完成后在命令行输入doxygen,如果出现帮助信息,说明安装成功,如果出现command not font则表明安装失败。之后使用sudo apt-get install doxygen-gui安装gui,就可以像windows那样使用图形化操作了。安装完成后使用doxygenwizard命令就可以打开doxygen了。sudo apt-get install doxygen sudo apt-get install doxygen-gui如何在ubuntu上创建快捷方式,自行百度。3、MacOS安装doxygen首先在官网下载MacOS版本的安装包。下载完成后,直接把他拖到application上面就可以了。4、Doxygen语法简介所谓Doxygen语法就是在写程序注视时候按照Doxygen语法规则来写注释。只有按照标准的注释规则来写注释,生成的文档才会非常偏亮,否则乱七八糟的。1.特殊命令简介命令字段名语法 @file文件名file [< name >] @brief简介brief { brief description } @author作者author { list of authors } @mainpage主页信息mainpage [(title)] @date年-月-日date { date description } @author版本号version { version number } @copyright版权copyright { copyright description } @param参数param [(dir)] < parameter-name> { parameter description } @return返回return { description of the return value } @retval返回值retval { description } @bug漏洞bug { bug description } @details细节details { detailed description } @pre前提条件pre { description of the precondition } @see参考see { references } @link连接(与@see类库,{@link www.google.com})link < link-object> @throw异常描述throw < exception-object> { exception description } @todo待处理todo { paragraph describing what is to be done } @warning警告信息warning { warning message } @deprecated弃用说明。可用于描述替代方案,预期寿命等deprecated { description } @example弃用说明。可用于描述替代方案,预期寿命等deprecated { description } 以上是写注释是可以加上的,但是实际操作中,并不需要这么多参数。2.文件注释一般放在文件开头/** * @file 文件名 * @brief 简介 * @details 细节 * @author 作者 * @version 版本号 * @date 年-月-日 * @copyright 版权 */示例3.结构体注释 /** * @brief 类的详细描述 */示例4.函数注释 /** * @brief 函数描述 * @param 参数描述 * @return 返回描述 * @retval 返回值描述 */实例5.常量/变量注释一般常量/变量可以有两种形式:常量/变量上一行注释常量/变量后注释//定义一个整型变量a int a; * @brief 定义一个整型变量a int a;int a; /*!< 定义一个整型变量a */ int a; /**< 定义一个整型变量a */ int a; //!< 定义一个整型变量a int a; ///< 定义一个整型变量a5、使用Doxygen软件上面说了半天就是想主要是让大家规范注释,然后使用Doxygen软件生成文档后会规范一些。我们以keil软件为例,使用Doxygen软件把我们的keil工程代码生成一份文档,这个文档包含了所有的函数与结构体,以及我们定义的所有函数,这样别人在看你的代码时就方便多了。1、打开Doxygen软件。2、选择运行doxygen的工作目录,就是你的keil工程在哪一个文件夹,这里就定位到那个文件夹。2、接着填写一些生成文档的一些信息,包括文档名称、简介、版本、源码文件路径、生成的文档路径。3.选择生成文档的格式,默认可以生成html和tatex格式的。4.最后再run选项卡中点击Run doxygen就可以生成了。5.在生成的目录中找到index.html文件打开即可。这样一个文档就生成好了。但是感觉有点不好看啊,没关系可以改一改参数,按照上面四张图的配置勾选,在再生成一次就可以了,现在可以看到树图就显示在最左边了,符合我们的观看形式。还可以把英文变成中文。再次生成就变成中文的了。这样一个文档说明就做好了。这时你只能在你的电脑上面找到index.html文件才能查看,如果想实时随时随地的查看就需要把他放到你的服务器上面了,可以通过网络访问。6、Nginx本地访问Nginx是一款是由俄罗斯的程序设计师Igor Sysoev所开发高性能的Web和反向代理服务器。首先下载Nginx。网址:http://nginx.org/en/download.html。如果打开Nginx.exe出现闪退。解决办法:nginx路径不许是英文的,不可以有中文路径。更改完后重新启动nginx.exe查看进程中是否有nginx。再在浏览器中输入地址localhost,正常打开即可。然后我们通过doxygen生成的文件放入nginx的html文件夹中。然后在浏览器输入:http://localhost/doxygen/index.html,就可以正常访问了。然后你可以把这个网页收藏起来,就再也不用每次去文件夹找index.html文件访问文档了,这样就非常的方便。当然你也可以把这个文件托管到gitee或者github上面就更加方便了。1.Doxygen并不处理所有的注释,doxygen重点关注与程序结构有关的注释,比如:文件、类、结构、函数、全局变量、宏等注释,而忽略函数内局部变量、代码等的注释。2.注释应写在对应的函数或变量前面。3.先从文件开始注释,然后是所在文件的全局函数、结构体、枚举变量、命名空间→命名空间中的类→成员函数和成员变量。4.Doxygen无法为DLL中定义的类导出文档。关注微信公众号:[果果小师弟],获取更多精彩内容!智果芯—服务于百万大学生和电子工程师

使用RTT代替UART,把你的JLink变成串口调试助手~

摘要:不知道大家在单片机开发中是如何打印调试信息的,大多数应该是用串口调试打印吧,在大多数的情况下,一般在制板和写代码时都会预留串口1做为调试打印用。但是在实际开发如果没有预留串口怎么办?其实我们的下载器是可以用来作为调试打印来用的,只是很多小伙伴不知道这个功能而已,今天就来说一下如何用调试器JLink来打印信息。1、JLink仿真调试器下载器五花八门,但是我只用JLink,小巧方便携带。对于单片机开发者一般所用的下载器基本就是JLink和ST-Link。这两者功能差不多,JLink是SEEGER公司的,ST-Link是ST公司的,而且只支持ST系列的芯片。只用JLink下载器调试,原因就是这玩意体积小,只有四根线,用起来太方便了,YYDS!2、安装JLink驱动下载链接:https://www.segger.com/downloads/jlink/。买回来JLink驱动后,一般卖家都会提供JLink的驱动程序,驱动安装完成后就可以下载调试程序了。当然我们现在要使用JLink的RTT功能,就需要在官网下载完整的Jlink包,最新版本的是V7.52版本的,当然别的版本也可以。下载完成后直接安装可以了。安装完成后你会在你的安装目录下看到如下内容:3、移植RTT安装完成就好办了,RTT源码包就在我们刚刚安装的JLINK驱动的目录里面。我的目录是:D:\Software\SEGGER\JLink_V644b\Samples\RTT解压后具体的目录是:D:\Software\SEGGER\JLink_V644b\Samples\RTT\SEGGER_RTT_V644b\RTT然后将这个RTT文件夹复制到我们的编写程序的工程文件夹中然后在项目中新建一个RTT分组,并将RTT文件夹中的两个.c文件添加进来。当然还要记得添加RTT的头文件路径到这里基本就移植成功了,是不是很简单,就是把RTT的源码添加到工程中即可,完全不需要修改别的什么操作。4、RTT打印输出接下来就可以打印输出了。#include "sys.h" #include "delay.h" #include "usart.h" #include "led.h" #include "SEGGER_RTT.h" int main(void) HAL_Init(); //初始化HAL库 Stm32_Clock_Init(336,8,2,7); //设置时钟,168Mhz delay_init(168); //初始化延时函数 LED_Init(); //初始化LED while(1) SEGGER_RTT_printf(0,"zhiguoxin666\r\n"); }编译没有错误之后连接好下载器之后打开JLink安装目录下的JLinkRTTViewer.exe按照如下配置将代码下载到单片机中就可以看到已经完美的打印了。5、RTT的使用技巧1、RTT缓冲大小有时候我们发现我们的信息不能完全的打印出来,可能是因为缓冲不够,默认缓冲区大小事1K字节,如果不够可以改大一点。2、多虚拟端口使用RTT支持向不同的虚拟端口中打印信息,使用方法如下。首先在RTT Viewer软件中分别打开三个虚拟端口:编写代码while(1) SEGGER_RTT_SetTerminal(0); SEGGER_RTT_printf(0,"zhiguoxin666,SEGGER RTT Terminal 0!\r\n"); SEGGER_RTT_SetTerminal(1); SEGGER_RTT_printf(0,"zhiguoxin666,SEGGER RTT Terminal 1!\r\n"); SEGGER_RTT_SetTerminal(2); SEGGER_RTT_printf(0,"zhiguoxin666,SEGGER RTT Terminal 2!\r\n"); delay_ms(1000); }编译、链接、下载,观察现象:3、修改打印字符颜色RTT支持不同颜色的字符显示。时用时在字符串前面加上对应颜色的宏定义就可以了。while(1) SEGGER_RTT_SetTerminal(0); SEGGER_RTT_printf(0,RTT_CTRL_TEXT_RED"zhiguoxin666,SEGGER RTT Terminal 0!\r\n"); SEGGER_RTT_SetTerminal(1); SEGGER_RTT_printf(0,RTT_CTRL_TEXT_GREEN"zhiguoxin666,SEGGER RTT Terminal 1!\r\n"); SEGGER_RTT_SetTerminal(2); SEGGER_RTT_printf(0,RTT_CTRL_TEXT_BLUE"zhiguoxin666,SEGGER RTT Terminal 2!\r\n"); delay_ms(1000); }编译、链接、下载,观察现象:4、使用printf重定向项目中使用printf的地方非常多,如果可以直接修改printf重定向到RTT组件,则会非常方便。使用的方法是直接使用RTT提供的API实现fputc。重定义fputc函数//重定义fputc函数 int fputc(int ch, FILE *f) SEGGER_RTT_PutChar(0, ch); return ch; }替换之前的代码:while(1) printf("zhiguoxin666 ,printf SEGGER RTT Terminal!\r\n"); delay_ms(1000); }编译、链接、下载结语:RTT和USRAT各有优点,要根据不同的情况选择,如果遇到一个显示项目没有预留串口用来调试打印信息,可以用别的方法。当然打印调试还有很多方法这只是其中的一种方法,如果你还有更好的方法,欢迎评论区留言哟~关注微信公众号:[果果小师弟],获取更多精彩内容!智果芯—服务于百万大学生和电子工程师

大端模式、小端模式、高字节序、低字节序、MSB、LSB

摘要:你知道内存是怎么读取数据的吗?知道数据是怎么一个一个字节发送的吗?是低字节先发还是高字节先发?是bit0先发还是bit7先发?是从低地址开始读还是从高地址开始读?看完本篇比应该就明白了~内存的读写永远从低地址开始读/写,从低到高!从低到高!从低到高!重要的话说三遍大端模式和小端模式大端模式和小端是实际的字节顺序和存储的地址顺序对应关系的两种模式。大端模式:高位字节存放在低地址中,低位字节存放在高地址中。最直观的字节序。小端模式:高位字节存放在高地址中,低位字节存放在低地址中。最符合人的思维的字节序,x86、ARM都这么搞(KEIL C51中,变量都是大端模式的;KEIL MDK中,变量是小端模式的。)。用图表示更加容易理解。以unsigned int value = 0x12345678为例,分别按照大端模式和小端模式存放在芯片中。内存地址0x000000010x000000020x000000030x00000004大端模式0x120x340x560x78小端模式0x780x560x340x12再换一种图示:同样以unsigned int value = 0x12345678为例,分别看看在两种字节序下其存储情况,我们可以用unsigned char buf[4]来表示value。不管是大端还是小端模式,我们在读取和存储数据的时候一定都是从内存的低地址依次向高地址读取或写入。另外注意,x86平台是小端的,ARM平台是小端的,而PowerPC平台是大端的。字节高低位一般左边为高位,右边为低位(这个高低来自于人类的阅读习惯,数字从左向右,表示由大到小)一个16位(双字节)的数据,比如0xFF1A,那么高位字节就是0xFF,低位是0x1A。如果是32位的数据,比如0x3F68415B。高位字(不是字节)是0x3F68,低位字是0x415B。右边是低位位,左边是高位(人的阅读习惯)LSB和MSB最高有效位(most mignificant bit,msb)指的是一个n位二进制数字中的n-1位,具有最高的权值2^(n-1)。 有时也指Most Significant Byte(MSB),指多字节序列中具有最大权重的字节。同理,最低有效位(least significant bit,lsb)和的是一个n位二进制数字中的0位,具有最低的权值2^0。有时也指Least Significant Byte(LSB),指多字节序列中具有最小权重的字节。所以0x12345678的最高有效字节就是0x12,最低有效字节就是0x78,这样明白了吧!举个栗子当选择模数转换器(ADC)时,最低有效位(LSB)这一参数的含义是什么?对于一个12位串行转换器,它会输出由1或0组成的12位数串。通常,转换器首先送出的是最高有效位(MSB)(即LSB + 11)。有些转换器也会先送出LSB。我们假设先送出的是MSB,然后依次送出MSB-1 (即 LSB + 10)和MSB -2(即LSB + 9)并依次类推。转换器最终送出MSB -11(即LSB)作为位串的末位。LSB这一术语有着特定的含义,它表示的是数字流中的最后一位,也表示组成满量程输入范围的最小单位。对于12位转换器来说,LSB的值相当于模拟信号满量程输入范围除以2^12 或 4096的商。如果用真实的数字来表示的话,对于满量程输入范围为4.096V的情况,一个12位转换器对应的LSB大小为1mV。但是,将LSB定义为4096个可能编码中的一个编码对于我们的理解是有好处的。高位先行msb 、低位先行lsb高位先行即在传输一个字节的时候先传输高位msb;低位先行即在传输一个字节的时候先传输低位lsb。高位先行和低位先行是针对串行数据传输方式来说的。常见的串行传输方式有串口(UAR)、I2C、SPI等。以串口传输方式为例,标准的串口传输方式是低位先行,芯片在通过TX引脚发送数据时,依次发送位0、位1……位7。串口传输是低位先行UART在数据传输时,协议规定了数据传输必须是低位先行,看下面的时序图你就知道了~IIC传输是高位先行IIC的数据和地址均以8位字节传输,MSB 在前。从图中可以清楚地看到:这一点也反映在代码中,我们随便找一个IIC的读字节和写字节的函数看看:void i2c_SendByte(uint8_t _ucByte) uint8_t i; /* 先发送字节的高位bit7 */ for (i = 0; i < 8; i++) if (_ucByte & 0x80) I2C_SDA_1(); I2C_SDA_0(); i2c_Delay(); I2C_SCL_1(); i2c_Delay(); I2C_SCL_0(); if (i == 7) I2C_SDA_1(); // 释放总线 _ucByte <<= 1; /* 左移一个bit */ i2c_Delay(); }从第7行代码中可以看到,在发送一个字节时,首先将要发送的字节与0x80进行与运算,取出最高位,然后循环左移8次就可以将一个字节数据发送出去了。你有没有想过为什么这里我们不把要发送的字节与0x01进行与运算,取出最低位,然后循环右移8次也可以将一个字节数据发送出去呢?答:因为我们说了I2C在数据传输时,协议规定了数据传输必须是高位先行,所以你要发送一个字节的数据肯定必须先取出最高位,然后循环左移将数据发出,如果你与上0x01,就是低位先行,虽然你也将一个字节发出去了,但是你发的是歪门邪道的数据,人家单片机也不认识,对吧?你品,你细品~同样在接收一个字节时,接收到的第1位认为是最高位,接收一个字节代码如下:uint8_t i2c_ReadByte(void) uint8_t i; uint8_t value; /* 读到第1个bit为数据的bit7 */ value = 0; for (i = 0; i < 8; i++) value <<= 1; I2C_SCL_1(); i2c_Delay(); if (I2C_SDA_READ()) value++; I2C_SCL_0(); i2c_Delay(); return value; }所有使用I2C的设备必须遵循I2C协议,必须都是高位先行的,这样才能实现通用性。怎么样?是不是又get到了一个小技巧~字节序、比特序字节序就是串行发送多字节时发送的顺序,比如value=0x12345678,按字节发送是0x12、0x34、0x56、0x78顺序还是0x78、0x56、0x34、0x12顺序。同理,比特序在bit层面进行排序,如果一个字节,指先发bit0还是bit7, 如果是一个Word型,先发bit31还是先发bit0串口是lsb优先,I2C是msb优先,这里的msb、lsb指的是比特序,二进制位的位置。区别于【字节序】通信中,先发送低字节,还是高字节的问题,那是字节序的MSB还是LSB,当然也有人混称上面所说的为大端发送big-endian、小端发送little-endian验证MCU平台存储方式?这里以STM32开发单片机的keil平台为例,以下代码如果打印0x04就是小端存储,如果0x01则是大端存储。因为0x04是低字节,读取数据是从低地址开始读,打印的是data的低地址,所以如果打印出的是0x04就表明低地址存储低字节,就为小端存储。明白了吗?#include "sys.h" #include "delay.h" #include "usart.h" #include "led.h" #include "key.h" #include "lcd.h" #include "SEGGER_RTT.h" #include "math.h" int main(void) HAL_Init(); //初始化HAL库 Stm32_Clock_Init(8,336,2,7);//设置时钟,168Mhz delay_init(168); //初始化延时函数 while(1) uint32_t data =0x01020304; char *p = (char*)&data; printf("0x0%x\n",*p);//看输出的是0x01还是0x04 delay_ms(1000); }编译、链接、下载,通过RTT查看试验结果:可以看出STM32是小端存储。总结:内存的读写永远从低地址开始读/写。大小端存储指字节在内存存储方式,X86、ARM平台都是小端存储(低-低),MSB/LSB只发送字节序或者比特序,串口是比特序LSB,IIC是比特序MSB。也有人将MSB、big-endian、大端发送都混为一谈,这时候一般指字节序上MSB。关注微信公众号:[果果小师弟],获取更多精彩内容!智果芯—服务于百万大学生和电子工程师

STM32寄存器版的基础知识—内存映射

@TOCSTM32F429芯片系统结构STM32F429 采用的是 Cortex-M4 内核,内核即 CPU,由 ARM公司设计。ARM 公司并不生产芯片,而是出售其芯片技术授权。芯片生产厂商(SOC)如 ST、TI、Freescale,负责在内核之外设计部件并生产整个芯片,这些内核之外的部件被称为核外外设或片上外设。如 GPIO、USART(串口)、I2C、SPI等都叫做片上外设。从上图我们可以清除的看到芯片和外设之间通过各种总线连接,其中主控总线有 8条,被控总线有 7 条。主控总线通过一个总线矩阵来连接被控总线, 总线矩阵用于主控总线之间的访问仲裁管理,仲裁采用循环调度算法。比如数据从Cotex-M4 到高速外设USB,数据交给在总线矩阵后,总线矩阵就会判给USB,然后通过USB所在的 AHB1 传输给USB。三大总线ICode:指令总线DCode:数据总线System:系统总线1、ICode 着重传输指令,DCode 和 System 着重传输数据,至于更详细的区分,不用关心。2、实际上 ICode、DCode 和 System 内部都包含三个部分,即地址总线、控制总线、数据总线。高速总线直接挂接在“总线矩阵上”的有哪些呢?1、ICode、DCode、System2、FLASH连接总线3、SRAM 连接总线4、高速外设连接总线 AHB1/AHB2/AHB35、连接“桥”的总线这些“高速总线”直接与“总线矩阵”连接在一起,其实这些高速总线实际上就是“总线矩阵”的延伸,或者说就是总线矩阵的一部分。高速外设和低速外设我们这里说“片内外设”时,暂都不包含 ROM(FLASH)和 SRAM。1、高速外设由于高速外设实在是太多了,一个总线不够,所以分了三个,分别是 AHB1/AHB2/AHB3,所有“高速外设”寄存器组分批挂接在 AHB1/AHB2/AHB3 上。2、低速外设“低速外设”的速度比较低,不能直接挂在“总线矩阵”上,所以先经过“桥”后再引出“低速总线”,挂在低速总线上。由于“低速外设”比较多,所以就分了两个低速总线,分别是 APB1/APB2,所有的“低速外设”分批挂接在 APB1/APB2 上。内存映射这张图太重要了,看懂这张图,你的STM32已经可以掌握40%了,下面就来着重讲解这一张图。这张图来自STM32F407参考手册第61页,由于原版是英文的,搞了一个翻译过来的版本。1、STM32存储空间芯片能访问的存储空间有多大,是由谁定的?这个是由芯片内 CPU 的地址总线的数量决来定的,STM32 芯片内部的地址总线为32 根,1、1 根地址线:可以传输的地址为 0 和 1 的,那么理论上就可以访问 2 个字节2、2 跟地址线:可以传输地址为 00、01、10、11,理论上可以访问 4 个字节3、3 个地址线:可以传输的地址为 000、001、010、011、100、101、110、111,理论上可以访问 8 个字节。4、32 根地址线:可以产生 00000000 00000000 00000000 00000000、...、11111111 11111111 11111111 11111111的 2^32个地址,范围刚好为 4G,所以我们就说STM32的32 根地址线,理论上可以访问4G字节的存储器空间。在上图的最右边可以看到STM32地址是从0x0000 0000到0xFFFF FFFF,这就是4GB的存储空间。但是STM32真的有4GB的存储空间吗?你在想啥呢?答案当然不是,我们的PC电脑也才4GB的内存。一个小小的单片机怎么可能有4GB的存储空间!这个4GB的是STM32理论分配的地址空间。也就是说实际上并不是有折磨大的存储单元。上图中第二排可以看到有很多预留的地址,这些地址并没有给他分配存储单元。所有的存储器都是与地址线连着的,但是实际上如果你只接了一个 1M 的存储器,而且是从0地址开始映射的,那么32 根地址线所产生的0~1M 的地址信号其实才是有意义的,因为这些地址信号才有对应真实的存储器,而所产生的1M 以上地址信号其实并无意义,因为并不对应真实的存储器。举个例子,政府给你化了10栋楼房的面积用来盖房子,但是实际上你没有那么多钱,只盖了3栋楼,其他的7栋房子预留的面积只能放在那里,凭什么不允许呢?这样说你应该明白了吧。STM32中的32是32根地址线的意思吗?答:不是,STM32的32不是32根地址线的意思,而是表示MCU芯片内部CPU在处理数据时,每次可以处理的数据位宽为32个bit,正是由于这个原因,STM32 内部的寄存器大小都是 32 位的,刚好等于位宽。某个芯片是 32 位的,但是它的地址线完全可以只有 16 根、或者 8 根,对于 STM32 来说,刚好碰巧的是,CPU 能够处理的数据“位宽”与地址线数量恰好都是 32,所以不少同学往往被搞迷糊了,认为是一回事。2、什么是存储器映射映射其实就是对应的意思。事实上存储器本身并不具备地址,将芯片理论上的地址分配给存储器,这就是存储器映射。举例理解:比如前面举的 1M 存储器的例子,这个 1M 存储器原本并没有地址,我将 1M 存储器映射到理论32 根地址线可以传输 4G 个地址信号,每个地址信号访问一个字节,4G 地址信号则可以访问 4G个字节,所以理论上的可访问范围为 4G。地址 0 往后的 1M 范围,这 1M 的存储器就有了 0—1M 的地址,地址线所产生的 0~1M 之间的地址信号,就可以访问 1M 的这个真实存储器。至于人家在生产芯片时,在工艺和技术上,具体是怎么实现我们所描述的映射的,这个我们无需关心。3、STM32F429的存储器映射STM32 的所有片内外设其实都是存储器,所以所有的这些存储器都需要被映射,只是理论上的 4G 范围远远大与实际的存储器空间,也就说实际的存储器空间并没有 4G。其实存储器是很贵的,一个 STM32 单片机如果有4G存储器的话,那就很贵了,而且单片机的产品根本不需要这么大的存储空间。理论上地址起始就是门牌号,存储中的每个字节就是房间,存储器生产出来后,这些房间是没有地址的(门牌号),映射的过程其实就是将这些门牌号分配给这些房间,分配好后,每个门牌号只能访问自己的房间,没有被分配的地址就是保留地址,所谓保留地址的意思就是,没有对应实际存储空间。可不可以保留一些地址不分配呢?当然可以,因为理论上可以有 4G 的地址,但是实际上不可能给你 4G 存储空间,否者这个单片机芯片你可买不起,你想PC机的内存也才 4G/8G,单片机怎么可能真的给你 4G 存储空间呢?寄存器映射存储器本身没有地址,给存储器分配地址的过程叫存储器映射,那什么叫寄存器映射?寄存器到底是什么?在存储器 Block2这块区域,也就是地址从0x4000 000—0x5FFF FFF这块区域,设计的是片上外设,它们以四个字节为一个单元,共32bit,每一个单元对应不同的功能,当我们控制这些单元时就可以驱动外设工作。可以找到每个单元的起始地址,然后通过 C语言指针的操作方式来访问这些单元,如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错,这时我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。那么如果我想往0x4002 0410这个地址写入数值0xFFFF FFFF,应该怎么操作呢?是不是只需要下面这一句话就可以了?*(unsigned int*)(0x4002 0410) = 0xFFFF;一直以为这句话很清楚,但是却发现有人看不懂这句话,那我来解释一下:首先编译器不知道0x4002 0410是一个啥东西,它可能表示小猫,也可能表示小狗。但是我们知道这个16进制数是一个地址对吧?那么怎么把它编程一个地址呢?是不是在它的前面加上(unsigned int*)变成*(unsigned int*)(0x4002 0410)就把这个数变成一个地址了?但是我们操作的是这个地址里面的内容,是不是再在前面加上一个星号变成*(unsigned int*)(0x4002 0410)就可以了,然后就可以给它赋值了:*(unsigned int*)(0x4002 0410) = 0xFFFF;地址重映射自举(bootstrap)计算机设备使用硬件加载的程序,用于初始化足够的软件来查找并加载功能完整的操作系统。也用来描述加载自举程序的过程。什么是单片机的自举,单片机的自举就是单片机的启动。我们说,单片机程序基本都是从 0 地址出开始运行的,F429 的0x0000 0000 - 0x001F FFFF 地址映射了到什么存储器上,那么就从该存储器上读取指令,开始运行。至于说 0x0000 0000 - 0x001F FFFF 到底映射在了什么存储器上,这个要看 F429 芯片 BOOT1、BOOT0 这两个引脚的电平值,说白了就是,通过 BOOT1 和 BOOT0 引脚的电平值,可以选择将 x00000000 - 0x001F FFFF 映射到不同的存储器上。STM32片内的FLASH分成两部分:主存储块、信息块。 主存储块(主Flash)用于存储程序,我们写的程序一般存储在这里。 信息块又分成两部分:系统存储器(系统FLASH)、选项字节。 系统存储器存储用于存放在系统存储器自举模式下的启动程序(BootLoader),当使用ISP方式加载程序时,就是由这个程序执行。这个区域由芯片厂写入BootLoader,然后锁死,用户是无法改变这个区域的。 选项字节存储芯片的配置信息及对主存储块的保护信息。 请留意主 flash 的地址“主FLASH”地址为 0x0800 0000 - 0x081F FFFF,大家回忆一下 STLINK 下载时的FLASH设置。是不是通过STLINK下载到了地址为0x0800 0000的地方,大小是0x0010 0000,也就是1MB。疑问:明明代码是下载到 0x80000000 往后的存储空间中,为什么说运行又是从 0x00000000地址运行的呢?为什么不是供 0x80000000 开始运行的呢?有关这个问题,就是我们说的单片机的自举。正常情况下都是映射到主FLASH上,所以都是从主FLASH上启动的,为了从FLASH启动,我们需要将代码下载到主FLASH上。什么是地址重映射如果 0x0000 0000 - 0x001F FFFF 之前是映射在系统存储器或者嵌入式 SRAM上的,现在改变BOOT0、BOOT1 的电平为 0、x。0x0000 0000 - 0x001FFFFF 就被重新映射在了主FLASH上,这就是单片机的地址重映射。重映射就是本来是和张三进行映射的的,现在改为了和李四映射。换句话说重映射就是0x0000 0000 - 0x001F FFFF(1MB)本来映射在系统存储器 0x1FFF 0000 - 0x1FFF 7A0F(30KB)上面,现在映射到了主FLASH 0x0800 0000 - 0x081F FFFF(1M8)上面。选择从主FLASH启动时,显然FLASH会被映射在了两片地址上。原本映射的地址(1MB):0x0800 0000 - 0x081F FFFF,进行 ST-LINK 下载时使用这个地址重映射的地址(1MB):0x0000 0000 - 0x001F FFFF,启动时CPU就是从重映射的地址读取指令这两片地址都是有效的,重映射到 FLASH 上后,CPU 从0地址开始运行时,就从 FLASH 上读取指令,当然前提是我们需要将代码下载FLASH中。这就解释了为什么我们在keil中设置好程序的下载地址为0x800 0000,但是单片机上电是确实从0开始执行。是因为我们在硬件上设置了BOOT0=1,BOOT1=X,从而导致了主FLASH区(也叫主闪存,大小1MB)被映射到了0x0000 0000 - 0x001F FFFF(1MB),故而代码是下载到 0x80000000 往后的存储空间中,却说运行又是从 0x00000000地址运行的。疑问:下载时,能不能使用 0x0000 0000 地址来下载?答:这个不行,因为下载时,0x0000 0000 - 0x001F FFFF 还没有被重映射到 flash 上,只能使用 0x0800 0000 来下载。上面说的是我们用JLink下载器下载代码,但是有时候我们还听说可以用串口来下载程序,这又是怎么回事?用串口下载程序,也就是我们说的ISP在系统中编程。从系统存储器启动,即STM32的ISP了。此时硬件电路B00T0=1,B00T1=0。由于串口不能直接把程序下载到主FLASH里面,所以需要使用到ST公司内嵌于系统存储区的Bootloader来引导把程序下载到主FLASH里面。JLink能直接把程序下载到内置的FLASH里面,是因为JLink下载器内部有Bootloader来引导把程序下载到FLASH里面。 程序下载完成后还需要配置BOOT引脚为BOOT0=0,BOOT1=X(即从主闪存存储器启动),复位后才能正常启动程序。如果你不修改BOOT引脚的话也就是B00T0=1,B00T1=0,那么0x0000 0000 - 0x001F FFFF是不是被重映射到系统存储器上面,而程序代码在主FLASH里面。你复位后程序肯定不能正常运行,只有在使用串口下载程序后配置BOOT引脚为BOOT0=0,BOOT1=X,复位后才能正常执行代码。你明白了吗?总结:使用JLink下载代码,JLink下载器内部的Bootloader将程序引导下载到主FLASH里面。使用串口下载代码,由于串口没有Bootloader,就要使用ST官方内置在芯片系统存储区的Bootloader代码,将程序引导下载止主FLASH。又因为程序是从0开始执行的,所以我们复位后运行程序时一定要让BOOT0=0,BOOT1=X,将0x00000000 - 0x001FFFFF是重映射到主FLASH我们代码存在的地方,从0开始执行代码。下图是使用FlyMcu串口下载程序,这个串口是USB-TTL,下载程序时让BOOT0=0,BOOT1=X即可。不是说在系统中编程需要将B00T0=1,B00T1=0吗?这是因为我们使用的是这个软件,这个软件可以通过DTR和RTS改变BOOT的引脚电平,达到不用修改BOOT引脚就可以下载运行代码,实际上是软件替我们做了改变BOOT引脚的操作,具体介绍可以看上面的说明。关于ISP与IAPISP(In System Programming)在系统编程,是指直接在目标电路板上对芯片进行编程,一般需要一个自举程序(BootLoader)来执行。ISP也有叫ICP(In Circuit Programming)、在电路编程、在线编程。IAP(In Application Programming)在应用中编程,是指最终产品出厂后,由最终用户在使用中对用户程序部分进行编程,实现在线升级。IAP要求将程序分成两部分:引导程序、用户程序。引导程序总是不变的。IAP也有叫在程序中编程。 ISP与IAP的区别在于,ISP一般是对芯片整片重新编程,用的是芯片厂的自举程序。而IAP只是更新程序的一部分,用的是电器厂开发的IAP引导程序。综合来看,ISP受到的限制更多,而IAP由于是自己开发的程序,更换程序的时候更容易操作。关注微信公众号:[果果小师弟],获取更多精彩内容!智果芯—服务于百万大学生和电子工程师

Keil MDK中使用AStyle插件对代码格式美化处理

摘要:通常我们写代码的时候,尤其是缩进和{}的使用,很多都需要自己手动去调整,如果有一个自动格式化代码的工具,每次编辑完代码,然后一键给将代码格式化,即省时又美观。为了解决这个问题,给大家推荐一个MDK插件—Astyle。一、下载AstyleAstyle全称Artistic Style,是一个免费,快速,小型的自动格式化程序,适用于C,C++,C++/CLI,Objective‑C,C#和Java源代码。官网地址:http://astyle.sourceforge.net/下载地址: https://sourceforge.net/projects/astyle/二、保存插件从官网下载的插件包后,将插件包放到你的keil安装目录下,放在哪里都可以,但是为了防止不小心删除,建议放到keil的安装目录下。我的存放路径是:三、配置MDK打开一个keil软件,在菜单栏Tools中选择Customize Tools Menu,然后按照下图一步步做;其中Menu Content就是自定义的用户命令,名称可以自己写,接下来看看我做的两个用户命令。1.格式化当前参数配置:(这个是自己参考别人的方法配置的)-n !E --style=ansi -p -s4 -S -f -xW -w -xw2.格式化工程参数配置:(这个是自己参考别人的方法配置的)-n "$E*.c" "$E*.h" --style=ansi -p -s4 -S -f -xW -w -xw -R!E表示的是当前获得焦点且正在编辑的文件。 $E*.c $E*.h代表当前获得焦点且正在编辑文件所在目录下所有.c和.h文件。使用的是Astyle默认格式来格式化文件,另外也可以自定义格式,自定义格式参考Astyle官网的帮助文档。四、实际效果演示1.使用插件前这里要注意的是,使用前需要先保存文件,不然不会生效。2.使用插件后五、主题美化最后不知道大家的MDK主题是啥样的,如果大家觉得我的注意不错的话,可以参考一下,将global.prop和global.prop.de两个文件复制到你的MDK安装目录UV4文件下,替换原来的文件即可。公众号后台回复:MDK美化,获取插件和配色文件。关注微信公众号:[果果小师弟],获取更多精彩内容!智果芯—服务于百万大学生和电子工程师

史上最全的LED点灯程序—使用STM32、FPGA、Linux点亮你的LED灯

摘要:不知道小伙伴们点亮过多少板子的LED灯,有很多小伙伴留言说讲一下stm32、fpga、liunx他们之间有什么不同,不同点很多,口说无凭,今天就来点亮一下stm32、fpga和liunx板子的led灯,大家大致看一下点灯流程和点灯环境以及点灯流程,就能大概的了解一下三者的区别,可以有选择的去学习!一、使用STM32点亮LED灯STM32从字面上来理解ST是意法半导体,M是Microelectronics的缩写,32 表示32位,合起来理解,STM32就是指ST公司开发的32位微控制器。在如今的32 位控制器当中,STM32可以说是最璀璨的新星,它受宠若娇,大受工程师和市场的青睐,无芯能出其右。首先使用STM32电亮一个led灯,大家现在回过头来看是不是非常的简单。STM32初始化流程1、使能指定GPIO的时钟。2、初始化GPIO,比如输出功能、上拉、速度等等。3、STM32有的IO可以作为其它外设引脚,也就是IO复用,如果要将IO作为其它外设引脚使用的话就需要设置 IO 的复用功能。4、最后设置GPIO输出高电平或者低电平。1、新建工程2、代码编写//LED IO初始化 void LED_Init(void) GPIO_InitTypeDef GPIO_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);//使能GPIOF时钟 //GPIOF9,F10初始化设置 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10;//LED0和LED1对应IO口 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//普通输出模式 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHz GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉 GPIO_Init(GPIOF, &GPIO_InitStructure);//初始化GPIO GPIO_SetBits(GPIOF,GPIO_Pin_9 | GPIO_Pin_10);//GPIOF9,F10设置高,灯灭 }3、编译代码4、配置下载器烧录代码二、使用FPGA点亮LED灯FPGA(Field Programmable Gate Array,简称 FPGA),译文:现场可编程门阵列,一种主要以数字电路为主的集成芯片,于1985年由Xilinx创始人之一 Ross Freeman发明,属于可编程逻辑器件PLD(Programmable Logic Device)的一种。真正意义上的第一颗FPGA芯片XC2064为Xilinx所发明,这个时间差不多比著名的摩尔定律晚20年左右,但是FPGA一经发明,后续的发展速度之快,超出大多数人的想象。计数器是在FPGA设计中最常用的一种时序逻辑电路,根据计数器的计数值我们可以精确的计算出FPGA内部各种信号之间的时间关系,每个信号何时拉高、何时拉低、拉高多久、拉低多久都可以由计数器实现精确的控制。而让计数器计数的是由外部晶振产生的时钟,所以可以比较精准的控制具体需要计数的时间。计数器一般都是从0开始计数,计数到我们需要的值或者计数满溢出后清零,并可以进行不断的循环。本例我们让计数器计数1s时间间隔,来实现led灯每隔1s闪烁一次的效果。LED灯硬件原理图 流水灯实验管脚分配 1、模块框图 模块框图 输入输出信号描述 2、RTL代码的编写开始RTL代码的编写,RTL代码编写出的模块叫RTL模块(后文中也称功能模块、可综合模块)。之所以叫RTL代码是因为用Verilog HDL在Resistances Transistors Logic(寄存器传输级逻辑)来描述硬件电路,RTL代码能够综合出真实的电路以实现我们设计的功能,区别于不可综合的仿真代码。`timescale 1ns/1ns //带标志信号的计数器 module counter parameter CNT_MAX = 25'd24_999_999 input wire sys_clk , //系统时钟50Mhz input wire sys_rst_n , //全局复位 output reg led_out //输出控制led灯 reg [24:0] cnt; //经计算得需要25位宽的寄存器才够500ms reg cnt_flag; //cnt:计数器计数,当计数到CNT_MAX的值时清零 always@(posedge sys_clk or negedge sys_rst_n) if(sys_rst_n == 1'b0) cnt <= 25'b0; else if(cnt < CNT_MAX) cnt <= cnt + 1'b1; cnt <= 25'b0; //cnt_flag:计数到最大值产生的标志信号 always@(posedge sys_clk or negedge sys_rst_n) if(sys_rst_n == 1'b0) cnt_flag <= 1'b0; else if(cnt == CNT_MAX - 1'b1) cnt_flag <= 1'b1; cnt_flag <= 1'b0; //led_out:输出控制一个LED灯,每当计数满标志信号有效时取反 always@(posedge sys_clk or negedge sys_rst_n) if(sys_rst_n == 1'b0) led_out <= 1'b0; else if(cnt_flag == 1'b1) led_out <= ~led_out; endmodule3、代码的分析和综合4、 查看RTL视图5、Testbench代码的编写`timescale 1ns/1ns module tb_counter(); //wire define wire led_out ; //reg define reg sys_clk ; reg sys_rst_n ; //初始化系统时钟、全局复位 initial begin sys_clk = 1'b1; sys_rst_n <= 1'b0; sys_rst_n <= 1'b1; //sys_clk:模拟系统时钟,每10ns电平翻转一次,周期为20ns,频率为50Mhz always #10 sys_clk = ~sys_clk; initial begin $timeformat(-9, 0, "ns", 6); $monitor("@time %t: led_out=%b", $time, led_out); //------------- counter_inst -------------- counter .CNT_MAX (25'd24 ) counter_inst .sys_clk (sys_clk ), //input sys_clk .sys_rst_n (sys_rst_n ), //input sys_rst_n .led_out (led_out ) //output led_out endmodule 6、ModelSim仿真波形7、上板验证程序下载完毕后,会看到板卡LED0不断闪烁,时间间隔为1秒。三、使用I.MX6ULL IO点亮LED嵌入式linux学习者大体可以分为两类,一类是进阶用户,主要指已经有大量mcu工作经验的开发者, 他们希望进阶到更有难度,薪资更高的mpu开发中去。另一类则是学生用户,主要是刚开始接触嵌入式开发的大学生群体。I.MX应用处理器包括I.MX8、I.MX7、I.MX6及I.MX28系列,被广泛应用于工业控制、汽车电子领域,久经市场考验。而且它的产品线非常丰富,用户熟悉其中一款产品后就能非常方便地迁移至不同的平台。一般拿到一款全新的芯片,第一个要做的事情的就是驱动其GPIO,控制其GPIO输出高低电平,我们学习I.MX6U也一样的,先来学习一下I.MX6U的GPIO。在学习I.MX6U的GPIO之前,我们可以对比一下STM32的GPIO初始化(如果没有学过 STM32 就不用回顾了),我们以最常见的STM32F103为例来看一下STM32的GPIO初始化,示例代码如下:void LED_Init(void) GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//使能 PB 端口时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //PB5 端口配置 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO 口速度 GPIO_Init(GPIOB, &GPIO_InitStructure); //根据设定参数初始化 GPIOB.5 GPIO_SetBits(GPIOB,GPIO_Pin_5); //PB.5 输出高 }STM32初始化流程1、使能指定GPIO的时钟。2、初始化 GPIO,比如输出功能、上拉、速度等等。3、STM32 有的 IO 可以作为其它外设引脚,也就是 IO 复用,如果要将 IO 作为其它外设引脚使用的话就需要设置 IO 的复用功能。4、最后设置GPIO输出高电平或者低电平。I.MX6U的GPIO一共有5组:GPIO1、GPIO2、GPIO3、GPIO4和GPIO5,其中GPIO1有32个IO,GPIO2有22个IO,GPIO3有29个IO、GPIO4有29个IO,GPIO5最少,只有12个IO,这样一共有124个GPIO。I.MX6ULL IO初始化流程1、使能时钟,CCGR0—CCGR6这7个寄存器控制着6ULL所有外设时钟的使能。为了简单,设置CCGR0~CCGR6这7个寄存器全部为0XFFFFFFFF,相当于使能所有外设时钟。2、IO复用,将寄存器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03的bit3~0设置为0101=5,这样GPIO1_IO03就复用为GPIO。3、寄存器IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03是设置GPIO1_IO03的电气属性。包括压摆率、速度、驱动能力、开漏、上下拉等。4、配置GPIO功能,设置输入输出。设置GPIO1_DR寄存器bit3为1,也就是设置为输出模式。设置GPIO1_DR寄存器的bit3,为1表示输出高电平,为0表示输出低电平。汇编由一条一条指令构成,指令就涉及到汇编指令。Int a,b; a=b;假设a地址为0X20,b地址为0x30LDR R0, =0X30 LDR R1, [R0] LDR R0, =0X20 STR R1, [R0]我们在使用汇编编写驱动的时候最常用的就是LDR和STR这两个指令。1、新建工程新建工程文件夹2、在VSCode中编写代码ubuntu中我们使用的是VScode编辑器来写代码,跟在windows中新建项目一样,新建项目、保存工作区,然后编写代码。3、编写代码.global _start /* 全局标号 */ _start: /* 1、使能所有时钟 ldf如果用大写就全部用大写,如果小写就全部用小写*/ ldr r0, =0X020C4068 //将寄存器CCGR0地址0X020C4068 存放到 寄存器R0 中 ldr r1, =0XFFFFFFFF //把寄存器x地址0Xffffffff存放到 寄存器r1 中 str r1, [r0]//把寄存器r1中的值(0XFFFFFFFF) 写入到寄存器r0里面的值作为地址的内存里面 ldr r0, =0X020C406C/*将寄存器CCGR1地址(0X020C4068) 存放到 寄存器R0 中*/ str r1, [r0] ldr r0, =0X020C4070 /* CCGR2 */ str r1, [r0] ldr r0, =0X020C4074 /* CCGR3 */ str r1, [r0] ldr r0, =0X020C4078 /* CCGR4 */ str r1, [r0] ldr r0, =0X020C407C /* CCGR5 */ str r1, [r0] ldr r0, =0X020C4080 /* CCGR6 */ str r1, [r0] /* 2、设置GPIO1_IO03复用为GPIO1_IO03 */ ldr r0, =0X020E0068 /* 将寄存器SW_MUX_GPIO1_IO03_BASE加载到r0中 */ ldr r1, =0X5 /* 设置寄存器SW_MUX_GPIO1_IO03_BASE的MUX_MODE为5 */ str r1,[r0] /* 3、配置GPIO1_IO03的IO属性 *bit 16:0 HYS关闭 *bit [15:14]: 00 默认下拉 *bit [13]: 0 kepper功能 *bit [12]: 1 pull/keeper使能 *bit [11]: 0 关闭开路输出 *bit [7:6]: 10 速度100Mhz *bit [5:3]: 110 R0/6驱动能力 *bit [0]: 0 低转换率 ldr r0, =0X020E02F4 /*寄存器SW_PAD_GPIO1_IO03_BASE */ ldr r1, =0X10B0 str r1,[r0] /* 4、设置GPIO1_IO03为输出 */ ldr r0, =0X0209C004 /*寄存器GPIO1_GDIR */ ldr r1, =0X0000008 str r1,[r0] /* 5、打开LED0 * 设置GPIO1_IO03输出低电平 ldr r0, =0X0209C000 /*寄存器GPIO1_DR */ ldr r1, =0 str r1,[r0] * 描述: loop死循环 loop: b loop .global _start @全局标号 /**/4、编译代码使用如下三条命令来编译代码arm-linux-gnueabihf-gcc -g -c leds.s -o led.o arm-linux-gnueabihf-ld -Ttext 0X87800000 led.o -o led.elf arm-linux-gnueabihf-objcopy -O binary -S -g led.elf led.bin最终生成了led.o led.elf led.bin三个文件5、烧写代码STM32中代码烧写到内部FLASH。IMX6ULL支持SD卡、EMMC、NAND、nor、SPI flash等启动。裸机例程选择烧写到SD卡里面。在ubuntu下向SD卡烧写裸机bin文件。烧写不是将bin文件拷贝到SD卡中,而是将bin文件烧写到SD卡绝对地址上。而且对于I.MX而言,不能直接烧写bin文件,比如先在bin文件前面添加头部。完成这个工作,需要使用正点原子提供的imxdownload软件。烧写的三个命令ls /dev/sd* -l chmod 777 imxdownload ./imxdownload led.bin /dev/sdbImxdownload使用方法,确定要烧写的SD卡文件,需要使用ls /dev/sd* -l命令来检测SD是哪一个文件,我的是/dev/sdb。给予imxdownload可执行权限:Chmod 777 imxdownload烧写:./imxdownload led.bin /dev/sdbImxdownlaod会向led.bin添加一个头部,生成新的load.imx文件,这个load.imx文件就是最终烧写到SD卡里面去的。这里要注意的是如果烧写的速度在几十MB/S左右的话,那么可能意味着烧写失败了。而且是因为SD卡没找到而导致烧写失败,这个问题只能重启 ubuntu解决。之后就可以从读卡器中把SD拔下来,然后插入到开发板中,将拨码开关拔止SD卡模式,供电之后,蓝色LED亮,红色LED灭,两秒钟之后红色LED亮。关注微信公众号:[果果小师弟],获取更多精彩内容!智果芯—服务于百万大学生和电子工程师

手把手教你 VSCode搭建STM32开发环境

摘要: 作为一个51单片机或STM32单片机的使用者,keil一直是我们的必备的一款工具之一。但keil的一些问题也一直存在,当然也有人用其他的比如STM32CubeIDE。但是今天推荐的是VScode+Keil Assistant插件,不需要很复杂的配置各种文件和环境变量,只需要一个插件即可!可以编译代码和下载程序。当我们的工程文件比较大的时候,编译一次代码需要很久可能会花费到四五分钟,但是我们用vscode编写和编译的话时间就会大大缩减,同时还支持右键的函数跳转和自动补齐功能。1、安装VScodeVScode大家应该不陌生了,Visual Studio Code(简称“VS Code”)是Microsoft在2015年4月30日Build开发者大会上正式宣布一个运行于 Mac OS X、Windows和 Linux 之上的,针对于编写现代Web和云应用的跨平台源代码编辑器,可在桌面上运行,并且可用于Windows,macOS和Linux。它具有对JavaScript,TypeScript和Node.js的内置支持,并具有丰富的其他语言(例如C++,C#,Java,Python,PHP,Go)和运行时(例如.NET和Unity)扩展的生态系统。在官网下载之后安装即可!2、安装C/C++插件VS Code安装完成之后,首先就要安装C/C++插件,点击软件最右边的扩展按钮,在出来的搜索框中输入C/C++,选择最上面一个然后点击安装即可,因为我这里已经安装过了,所以现实的是卸载。3、安装Keil Assistant插件C/C++插件安装完成之后,首先就要安装1Keil Assistant插件,同样点击软件最右边的扩展按钮,在出来的搜索框中输入keil,选择最上面一个Keil Assistant然后点击安装即可。添加keil可执行文件UV4.exe的绝对路径,这里有两个路径,上面是C51的,下面是MDK的,这里以STM32为例,添加的是我电脑上的MDK的可执行文件的路径,如下:这里在桌面找到Keil的图标,右键查看文件所在位置,就能看到keil的可执行文件的路径,把它复制到上面就可以了。至此VScode的MDK环境就搭建好了,是不是很简单。4、用vscode打开keil工程当插件安装完成之后文件界面会出现KEIL UVISION PROJECT,然后点击右边的+加号。选择我们要打开的keil文件,和用MDK打开工程一样,需要打开后缀名为.uvprojx的文件。之后项目就打开了!5、编译、下载程序这个插件是可以进行编译,烧录的。不需要额外添加其他的插件。提供了3个按钮,分别代表 编译,下载,重新编译。编译所需要的工具下载器的配置,是在MDK中配置的,也就是说你在MDK中配置好Debug,在VScode中就可以直接点击下载按钮下载程序了,是不是很方便!5、常用操作1、编译,烧录:提供了 3 个按钮,分别代表 编译,下载,重新编译2、保存和刷新:在 Keil 上添加/删除源文件,更改,配置项目,更改完毕后点击 保存所有,插件检测到 keil 项目变化后会自动刷新项目3、打开源文件:单击源文件将以预览模式打开,双击源文件将切换到非预览模式打开4、切换 c/c++ 插件的配置:点击目标名称在多个 c/c++ 配置中切换5、切换 keil Target:点击项目的切换按钮,可以在多个Keil Target 之间切换展开引用:在编译完成后,可以点击源文件项的箭头图标展开其引用(仅支持 ARM 项目)官方简述下面是官方对这个插件的描述:VScode上的Keil辅助工具,与c/c++ 插件配合使用。能够为Keil项目提供语法高亮、代码片段的功能,并支持对keil项目进行 编译、下载。仅支持 Keil uVison 5 及以上版本。仅支持Windows平台。功能特性1、加载Keil C51/ARM 项目,并以Keil项目资源管理器的展示方式显示项目视图。2、自动监视keil项目文件的变化,及时更新项目视图。3、通过调用Keil命令行接口实现 编译,重新编译,烧录keil项目。4、自动生成c_cpp_properties.json文件,使C/C++插件的语法分析能正常进行。结语:编译工具千千万,适合自己最重要。小伙伴你们觉得这个插件好用吗?关注微信公众号:[果果小师弟],获取更多精彩内容!智果芯—服务于百万大学生和电子工程师

串口网口16进制发送的和ASCII发送以及16进制接收和ASCII接收区别

我们在工控软件中,会经常使用到网口和串口,去接受和发送数据。通常我们发送数据的模式有两种,一种16进制,一种是ASCII码。16进制的的经常会用来和仪器PLC等设备通讯。ACSII码是一种文本模式。1、当我们不点选16进制时,按文本模式发送。这是我们输入的文本区的内容是一个个字符。比如输入06,这时06为‘0’和‘6’两个字符。发送的时候会将字符‘0’的ASCII码和字符‘6’的ASCII码发送出去,即是0x30和0x36。当我们以文本模式(ASCII)接收时就会收到06,当我们以16进制(HXE)接收时就会收到0X30,0X36,其中0x代表16进制,不会在串口调试助手上显示出来,只会显示 30 362、当我们按16进制发送06时,这时06为一个16进制数即0x06。当我们以文本模式(ASCII)接收时就会收到的为乱码,因为16进制的0x06的ASCII码是不可显示字符为ACK。当我们以16进制(HXE)接收时就会收到0X06,其中0x代表16进制,不会在串口调试助手上显示出来,只会显示06这就是为什么按16进制发送的效率要高于ASCII码的效率。从中我们也可以看出计算机底层发送数据是一个个数。注意:串口和网口接收回来,当你用char 类型的buf去接收的时候,其实已经进行一次转换了。这是它的十进制范围是-128—127。如果我们要将其变成无符号的数就要用byte类型的buf去接收,或者用char接收,强制转化为unsigned char类型。这样的它的十进制范围就变成了0~255。这样你就可以用多个buf 组成16,32位等数据了。加粗样式最后 总结 计算机底层发送数据是一个个数。接收来之后,要我们自己按照自己的方式转换。常见的转换的函数用 itoa strtoul strtol atoi atof。多个字节转化要使用移位,取反等操作。都要大四了还搞不清这个概念也真是丢脸。首先,底层的数据传输都是字节流,所以不管选择什么方式,都会被分解为一个一个的字节。1、选择Hex发送就代表你要发送的内容是纯数字,由程序完成String到Int再到Byte的转化。所以你应该保证每个你要发送的数都是两位的,如果是7就应该写07,因为程序会每两位每两位地读。如果你选择了Hex发送,而输入的又是字符,比如你写了ab,那么就会被程序读为16进制的AB。这就是不同的概念了,无论你选择什么方式显示都不能得到原来的ab了。2、选择ASCII发送就代表你要发送的是字符串,这时候程序就会一位一位地读,比如你写了1234,在字节流中传递的就是123对应的ASCII码,31,32,33,34(十六进制的)。比较而言,在Hex发送模式下,写了1234,会被发送的就是12,34,如果是01020304那就是01,02,03,04。这个时候,你写ab就会发送相应的ASCII码61,62,其他字符同理。到这里,数据已经发送出去了,接下来就是显示的问题。是显示模式,不是解析,不存在解析。3、选择Hex显示就是把字节转化为16进制整型,你收到的是12,34,就显示为12,34,你收到31,32,33,34,也显示为31,32,33,34,如果收到AB呢,那也是AB。4、选择ASCII显示呢,就会把你接收到的十六进制转化为对应的字符,比如你收到了31,就会显示为1。这种模式下可能会出现乱码,原因就是ASCII码只从0-7f。如果你在十六进制发送模式下发送了字符,比如发送了ab,那你就会收到AB,这个并没有ASCII码对应的字符。所以在Hex模式下如果输入字符,是无论如何接收不到正确的数据的,其他方式那就随意了。重要的是,方式的选择改变的不是数据本身,而是数据的表现形式。参考文章:https://blog.csdn.net/u010154491/article/details/58592831https://blog.csdn.net/weixin_30372371/article/details/95550949https://blog.csdn.net/wuan584974722/article/details/54460220

STM32三种BOOT启动模式详解(全网最全)

一、三种boot启动模式一般来说就是指我们下好程序后,重启芯片时,SYSCLK的第4个上升沿,BOOT引脚的值将被锁存。用户可以通过设置BOOT1和BOOT0引脚的状态,来选择在复位后的启动模式。1、第一种方式(boot0 = 0):Flash memory启动方式启动地址:0x08000000 是STM32内置的Flash,一般我们使用JTAG或者SWD模式下载程序时,就是下载到这个里面,重启后也直接从这启动程序。基本上都是采用这种模式。2、第二种方式(boot0 = 1;boot1 = 0):System memory启动方式启动地址:0x1FFF0000从系统存储器启动,这种模式启动的程序功能是由厂家设置的。一般来说,这种启动方式用的比较少。系统存储器是芯片内部一块特定的区域,STM32在出厂时,由ST在这个区域内部预置了一段BootLoader, 也就是我们常说的ISP程序, 这是一块ROM,出厂后无法修改。一般来说,我们选用这种启动模式时,是为了从串口下载程序,因为在厂家提供的BootLoader 中,提供了串口下载程序的固件,可以通过这个BootLoader将程序下载到系统的Flash中。但是这个下载方式需要以下步骤:1、将BOOT0设置为1,BOOT1设置为0,然后按下复位键,这样才能从系统存储器启动BootLoader2、最后在BootLoader的帮助下,通过串口下载程序到Flash中3、程序下载完成后,又有需要将BOOT0设置为GND,手动复位,这样,STM32才可以从Flash中启动可以看到, 利用串口下载程序还是比较的麻烦,需要跳帽跳来跳去的,非常的不注重用户体验。3、第三种方式(boot0 = 1;boot1 = 1):SRAM启动方式。启动地址:0x20000000 内置SRAM,既然是SRAM,自然也就没有程序存储的能力了,这个模式一般用于程序调试。假如我只修改了代码中一个小小的 地方,然后就需要重新擦除整个Flash,比较的费时,可以考虑从这个模式启动代码(也就是STM32的内存中),用于快速的程序调试,等程序调试完成后,在将程序下载到SRAM中。二、关于启动地址理论上,CM3中规定上电后CPU是从0地址开始执行,但是这里中断向量表却被烧写在0x0800 0000地址里(Flash memory启动方式),那启动时不就找不到中断向量表了?既然CM3定下的规矩是从0地址启动,SMT32当然不能破坏ARM定下的“规矩”,所以它做了一个启动映射的过程,就是和芯片上总能见到的BOOT0和BOOT1有关了,当选择从主Flash启动模式后,芯片一上电,Flash的0x0800 0000地址被映射到0地址处,不影响CM3内核的读取,所以这时的CM3既可以在0地址处访问中断向量表,也可以在0x0800 0000地址处访问中断向量表,而代码还是在0x0800 0000地址处存储的。三、关于flash死锁的解决办法(Flash memory启动方式)开发调试过程中,由于某种原因导致内部Flash锁死,无法连接SWD以及JTAG调试,无法读到设备,可以通过修改BOOT模式重新刷写代码。修改为BOOT0=1,BOOT1=0即可从系统存储器启动,ST出厂时自带Bootloader程序,SWD以及JTAG调试接口都是专用的。重新烧写程序后,可将BOOT模式重新更换到BOOT0=0,BOOT1=X即可正常使用。关注微信公众号:[果果小师弟],获取更多精彩内容!智果芯—服务于百万大学生和电子工程师

STM32单片机修改寄存器的位操作方法(全网最全)

使用 C语言对寄存器赋值时,我们常常要求只修改该寄存器的某几位的值,且其它的寄存器位不变,这个时候我们就需要用到 C 语言的位操作方法了。1. 把变量的某位清零此处我们以变量 a代表寄存器,并假设寄存器中本来已有数值,此时我们需要把变量a 的某一位清零,且其它位不变。//定义一个变量 a = 1001 1111 b (二进制数) unsigned char a = 0x9f; //对 bit2 清零 a &= ~(1<<2); //括号中的 1 左移两位,(1<<2)得二进制数:0000 0100 b //按位取反,~(1<<2)得 1111 1011 b //假如 a 中原来的值为二进制数: a = 1001 1111 b //所得的数与 a 作”位与&”运算,a = (1001 1111 b)&(1111 1011 b), //经过运算后,a 的值 a=1001 1011 b // a 的 bit2 位被被零,而其它位不变。2. 把变量的某几个连续位清零由于寄存器中有时会有连续几个寄存器位用于控制某个功能,现假设我们需要把寄存器的某几个连续位清零,且其它位不变。//若把 a 中的二进制位分成 2 个一组 //即 bit0、bit1 为第 0 组,bit2、bit3 为第 1 组, // bit4、bit5 为第 2 组,bit6、bit7 为第 3 组 //要对第 1 组的 bit2、bit3 清零 a &= ~(3<<2*1); //括号中的 3 左移两位,(3<<2*1)得二进制数:0000 1100 b //按位取反,~(3<<2*1)得 1111 0011 b //假如 a 中原来的值为二进制数: a = 1001 1111 b //所得的数与 a 作”位与&”运算,a = (1001 1111 b)&(1111 0011 b), //经过运算后,a 的值 a=1001 0011 b // a 的第 1 组的 bit2、bit3 被清零,而其它位不变。 //上述(~(3<<2*1))中的(1)即为组编号;如清零第 3 组 bit6、bit7 此处应为 3 //括号中的(2)为每组的位数,每组有 2 个二进制位;若分成 4 个一组,此处即为 4 //括号中的(3)是组内所有位都为 1 时的值;若分成 4 个一组,此处即为二进制数“1111 b” //例如对第 2 组 bit4、bit5 清零 a &= ~(3<<2*2);3. 对变量的某几位进行赋值。寄存器位经过上面的清零操作后,接下来就可以方便地对某几位写入所需要的数值了,且其它位不变,这时候写入的数值一般就是需要设置寄存器的位参数。//a = 1000 0011 b //此时对清零后的第 2 组 bit4、bit5 设置成二进制数“01 b ” a |= (1<<2*2); //a = 1001 0011 b,成功设置了第 2 组的值,其它组不变4. 对变量的某位取反某些情况下,我们需要对寄存器的某个位进行取反操作,即 1 变 0 ,0变 1,这可以直接用如下操作,其它位不变,见代码清单 5-4。//a = 1001 0011 b //把 bit6 取反,其它位不变 a ^=(1<<6); //a = 1101 0011 b

物联网中常用的数据处理方法

取出某一段数据中的某部分数据/*********************************************************** 函数名称:Find_string(char *pcBuf,char*left,char*right, char *pcRes) 函数功能:寻找特定字符串 入口参数: char *pcBuf 为传入的字符串 char*left 为搜索字符的左边标识符 例如:"[" char*right 为搜索字符的右边标识符 例如:"]" char *pcRes 为输出转存的字符串数组 返回值:用来校验是否成功,无所谓的。 备注: left字符需要唯一,right字符从left后面开始唯一即可 服务器下发命令举例:+MQTTPUBLISH: 0,0,0,0,/device/NB/zx99999999999999_back,6,[reastrobot] 如要取出 IMEI:4569874236597\r\n 中的数字4569874236597 ***********************************************************/ int Find_string(char *pcBuf,char *left,char *right, char *pcRes) char *pcBegin = NULL; char *pcEnd = NULL; pcBegin = strstr(pcBuf, left);//取出左边数据 pcEnd = strstr(pcBegin+strlen(left), right);//扫描右边标识 if(pcBegin == NULL || pcEnd == NULL || pcBegin > pcEnd) printf("string name not found!\n"); return 0; pcBegin += strlen(left); memcpy(pcRes, pcBegin, pcEnd-pcBegin); return 1; }strstr使用方法https://www.runoob.com/cprogramming/c-function-strstr.html解释:C 库函数char *strstr(const char *haystack, const char *needle) 在字符串 haystack 中查找第一次出现字符串 needle 的位置,不包含终止符 '\0'。声明下面是 strstr() 函数的声明。char *strstr(const char *haystack, const char *needle)参数haystack -- 要被检索的 C 字符串。 needle -- 在 haystack 字符串内要搜索的小字符串。返回值该函数返回在 haystack 中第一次出现 needle 字符串的位置,如果未找到则返回 null。实例下面的实例演示了 strstr() 函数的用法。实例#include <stdio.h> #include <string.h> int main() const char haystack[20] = "RUNOOB"; const char needle[10] = "NOOB"; char *ret; ret = strstr(haystack, needle); printf("子字符串是: %s\n", ret); return(0); }让我们编译并运行上面的程序,这将产生以下结果:子字符串是: NOOBmemcpy()使用方法https://www.runoob.com/cprogramming/c-function-memcpy.html一、memsetvoid *memset(void *s, int ch, size_t n);函数解释:将s中当前位置后面的n个字节(typedef unsigned int size_t)用 ch 替换并返回 s 。memset:作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方。memset()函数原型是 extern void *memset(void *buffer, int c, int count) buffer:为指针或是数组, c:是赋给buffer的值, count:是buffer的长度.memset() 函数常用于内存空间初始化。如:char str[100];memset(str,0,100);memset的作用就是把str这个数组中的值初始化为0二、sprintf# include<stdio.h> int sprintf(char *str, char *format ,...);功能:根据参数format字符串来转换并格式化数据,然后将结果输出到str指定的空间中,直到出现字符串结束符'\0'为止。参数:参数列表str:字符串首地址 format:这是字符串,包含了要被写入到字符串str的文本,可以包含嵌入的format标签 ...:根据不同的format字符串,函数可能需要一系列的附加参数返回值:成功:实际格式化字符的个数 失败:-1例子: 编译环境为VS2017,是在c环境下编译,同时为了不使sprintf函数出错,需要 右键单击工程名——>属性——>C/C++——>常规——>SDL检查(将SDL检查改为否) #include<stdio.h> #include<string.h> #define a "hello" #define b "world" int main() //1.格式化字符串 char buf[1024]; sprintf(buf, "hello %s", "world"); printf("buf:%s\n", buf); printf("len:%d\n\n", strlen(buf)); //2.拼接字符串 char temp[1024]; char *s1 = "hello"; char *s2 = "world"; memset(temp, 0, 1024); sprintf(temp, "%s %s", s1,s2); printf("buf:%s\n", temp); printf("len:%d\n\n", strlen(temp)); //2.1拼接字符串 char str[1024]; sprintf(str, "%s %s", a, b); printf("buf:%s\n", str); printf("len%d\n\n", strlen(str)); //3.数字转换成字符串打印出来 char num[1024]; int number = 666; memset(num, 0, 1024); sprintf(num, "%d", number); printf("buf:%s\n", num); printf("len:%d\n\n", strlen(num)); //4.设置宽度右对齐 char buffer[1024]; memset(buffer, 0, 1024); sprintf(buffer, "%8d", number); printf("buf:%s\n", buffer); printf("len:%d\n\n", strlen(buffer)); //5.设置宽度左对齐 memset(buffer, 0, 1024); sprintf(buffer, "%-8d", number); printf("buf:%s\n", buffer); printf("len:%d\n\n", strlen(buffer)); //6.转换成16进制字符串 小写 memset(buffer, 0, 1024); sprintf(buffer, "0x%x", number); printf("buf:%s\n", buffer); printf("len:%d\n\n", strlen(buffer)); //6.转换成8进制字符串 memset(buffer, 0, 1024); sprintf(buffer, "0%o", number); printf("buf:%s\n", buffer); printf("len:%d\n\n", strlen(buffer));

STM32F103系列单片机的FLASH和RAM大小

STM32F103C8T6CPU:STM32F103RCT6,LQFP64,FLASH:64KB,RAM:20KBflash起始地址为0x8000000,大小为0x10000(16进制)—>65536字节(10进制)—>64KBRAM起始地址为0x2000000,大小为0x5000(16进制)—>20480字节(10进制)—>20KBSTM32F103RCT6CPU:STM32F103RCT6,LQFP64,FLASH:256KB,SRAM:48KB;flash起始地址为0x8000000,大小为0x4000(16进制)—>262144字节(10进制)—>256KBRAM起始地址为0x2000000,大小为0xC000(16进制)—>49125字节(10进制)—>48KBSTM32F103VET6CPU:STM32F103VET6,LQFP100,FLASH:512KB,SRAM:64KB;flash起始地址为0x8000000,大小为0x80000(16进制)—>524288字节(10进制)—>512KBRAM起始地址为0x2000000,大小为0x10000(16进制)—>65536字节(10进制)—>64KBSTM32F103ZET6CPU:STM32F103ZET6,LQFP144,FLASH:512KB,SRAM:64KB;flash起始地址为0x8000000,大小为0x80000(16进制)—>524288字节(10进制)—>512KBRAM起始地址为0x2000000,大小为0x10000(16进制)—>65536字节(10进制)—>64KB

华为太空人智能表盘代码仅需100行?

摘要:我来告诉你他到底有什么秘密,风儿风儿吹风儿风儿吹吹!听说最近太空人智能表盘很火啊,那么如何用C++做一个好玩的智能太空人表盘呢?安排!软件工具:Vsiual studio 201x1、安装Vsiual studio首先在官网下载安装Vsiual studiohttps://visualstudio.microsoft.com/zh-hans/下载完成后傻瓜式安装即可,如果你电脑上已经安装过Vsiual studio软件了这一步就可以忽略了。2、安装EasyX图形库由于太空人表盘界面需要用到很多画线画图函数,这里我们需要安装EasyX图形库。graphics.h是TC的针对DOS下的一个C语言图形库,如果要用的话应该用TC的编译器来编译,VC++环境有其它的针对Windows的图形库。分为:像素函数、直线和线型函数、多边形函数、填充函数等。如果有需要在VC及VS环境中使用graphics.h的功能,可以下载EasyX图形库,这是一个C++的图形库,如果一定要在C语言环境下使用graphics.h,可以使用Windows GDI。下载方法在网址:https://easyx.cn/downloads/ 下载graphics.h头文件安装包。安装包的图标如下图安装包下载完成后按照安装向导安装,软件会自动检测你电脑上安装的Vsiual studio版本,因为我用的是2017版本,直接点击安装即可,这样EasyX图形库就安装到你的电脑中了。注意:如果不安装这个,接下来新建工程会报错没有graphics.h这个头文件。3、新建工程在所有环境都安装成功之后,就可以来新建一个工程了。打开Vsiual studio 2017软件,按照下图的配置新建一个空的新工程。在工程的源文件和头文件中添加新建项,也就是工程所需的.h文件和.c文件。这两个文件我会放到文末!这样工程就新建完毕了!4、编写代码由于windows生成的窗口是方形的,而智能表盘的形状是圆的,所以第一步就要修改窗口的样式4.1初始化界面为圆形void SetWindowNewStyle(int w, int h) // 去掉标题 SetWindowLong(GetHWnd(), GWL_STYLE, GetWindowLong(GetHWnd(), GWL_STYLE) & ~WS_CAPTION); // 初始化界面为圆形 SetWindowRgn(GetHWnd(), CreateEllipticRgn(0, 0, w, h), true); }4.2 加载图片太空表盘的转动动图是由一张一张的图片循环播放的效果,所以使用LoadImage函数,可以装载图标,光标,或位图,支持bmp,jpg,gif,emf,wmf,ico格式。void loadImg() mciSendString("open ./images/风儿吹.mp3", NULL, 0, NULL); mciSendString("play ./images/风儿吹.mp3 repeat", NULL, 0, NULL); char fileName[50] = { 0 }; for (int i = 0; i < 30; i++) sprintf_s(fileName, "./images/guoguoxiaoshidi (%d).jpeg", i + 1); loadimage(spaceMan + i, fileName, 140, 130); loadimage(&other[0], "./images/xinlv.jpg", 60, 60);//心率 loadimage(&other[1], "./images/sun.jpg", 40, 40);//太阳 loadimage(&other[2], "./images/shoes.jpg", 40, 40);//鞋子 loadimage(&other[3], "./images/shang.jpg", 30, 30);//上箭头 loadimage(&other[4], "./images/xia.jpg", 30, 30);//下箭头 loadimage(&other[5], "./images/rocket.jpg", 40, 40);//火箭 }4.3 绘制汉字与直线这里要注意的是高版的VS默认不让使用scanf,fopen等函数,如果使用会报scanf,fopen等函数不安全。而代替其函数的是scanf_s,fopen_s等函数,后边有个"_s"的形式。想要使用可右击工程 - 属性 - 配置属性 - C/C++ - 预处理器。增加下面两行,命令即可!_CRT_SECURE_NO_DEPRECATE _CRT_SECURE_NO_WARNINGS下面就是具体的画图画线函数void gameDraw() setbkcolor(RGB(255, 0, 0)); cleardevice(); //绘制表盘 setlinecolor(RGB(0, 0, 0));//设置边框颜色 setlinestyle(PS_SOLID, 30); setfillcolor(RGB(255, 255, 255));//设置圆的填充白色 fillellipse(0, 0, WIN_SIZE, WIN_SIZE);//绘制一个圆 //绘制线条 setlinestyle(PS_SOLID, 4); setlinecolor(BLACK); //最上面竖线 line(WIN_HALF - 30, 20, WIN_HALF - 30, 130); //横线x2 line(WIN_HALF - 195, WIN_HALF - 120, WIN_HALF + 195, WIN_HALF - 120); line(WIN_HALF - 195, WIN_HALF + 120, WIN_HALF + 195, WIN_HALF + 120); //下面线条x3 line(WIN_HALF + 80, WIN_HALF + 120, WIN_HALF + 80, WIN_HALF + 175); line(WIN_HALF + 80, WIN_HALF + 175, WIN_HALF - 60, WIN_HALF + 175); line(WIN_HALF - 60, WIN_HALF + 175, WIN_HALF - 60, WIN_HALF + 175 + 48); setbkmode(TRANSPARENT); //左上空气湿度90% setTextStyle(55, 23, "Arial"); settextcolor(BLACK); outtextxy(WIN_HALF - 155, 75, "90%"); drawImg(other + 5, WIN_HALF - 90, 35); //火箭 putimage(WIN_HALF - 90, 35, other + 5); setTextStyle(25, 15, "黑体"); outtextxy(WIN_HALF - 25, 35, "空气良好"); setTextStyle(25, 13, "宋体"); outtextxy(WIN_HALF - 25, 65, "晴天"); outtextxy(WIN_HALF - 25, 95, "25℃"); outtextxy(WIN_HALF + 38, 65, "26°"); outtextxy(WIN_HALF + 38, 95, "17°"); drawImg(other + 4, WIN_HALF + 73, 60); drawImg(other + 3, WIN_HALF + 73, 90); drawImg(other + 1, WIN_HALF + 105, 70); putimage(WIN_HALF + 73, 60, other + 4); putimage(WIN_HALF + 73, 90, other + 3); putimage(WIN_HALF + 105, 70, other + 1); setTextStyle(37, 17, "宋体"); outtextxy(100, WIN_HALF + 130, "睡眠"); outtextxy(WIN_HALF + 90, WIN_HALF + 130, "距离"); outtextxy(50, WIN_HALF-40, "武汉"); setTextStyle(40, 15, "Arial"); outtextxy(185, WIN_HALF + 125, "7h30m"); outtextxy(215, WIN_HALF + 180, "9.88km"); setTextStyle(25, 13, "宋体"); outtextxy(60, WIN_HALF + 30, "80~128"); drawImg(&other[0], 65, WIN_HALF + 50); //心率图 putimage(65, WIN_HALF + 50, other + 0); setTextStyle(40, 15, "Arial"); outtextxy(135, WIN_HALF + 60, "92"); // 步数 drawImg(&other[2], WIN_HALF + 65, WIN_HALF + 65); putimage(WIN_HALF + 65, WIN_HALF + 65, &other[2]); outtextxy(WIN_HALF + 125, WIN_HALF + 75, "9527"); //时间、日期相关 time_t timep = time(NULL); //获取当前时间 struct tm* p = localtime(&timep); //把时间转成格式化时间 setTextStyle(25, 12, "宋体"); outtextxy(WIN_HALF + 110, WIN_HALF - 20, "四月六号"); char fileName[40] = { 0 }; sprintf_s(fileName, "周%s %d-%d", week[p->tm_wday], p->tm_mon + 1, p->tm_mday); outtextxy(WIN_HALF + 110, WIN_HALF + 10, fileName); // 获取字体 setTextStyle(100, 40, "Arial"); char szBuf[40] = { 0 }; sprintf_s(szBuf, "%d:%02d", p->tm_hour, p->tm_min); outtextxy(105, 120, szBuf); setTextStyle(55, 23, "Arial"); sprintf(szBuf, "%02d", p->tm_sec); outtextxy(335, 160, szBuf); }4.4 main函数int main() initgraph(WIN_SIZE, WIN_SIZE/*,EW_SHOWCONSOLE*/); SetWindowNewStyle(WIN_SIZE, WIN_SIZE); loadImg(); BeginBatchDraw();//双缓冲 防止闪屏 while (true) gameDraw(); animation(); mouseEvent(); FlushBatchDraw(); EndBatchDraw(); return 0; }5、效果展示在显示动画的同时电脑也会播放音乐哟!小伙伴赶紧下载工程玩玩去吧!

STM32端口IO方向设置问题的

例程:STM32F103系列 I2C软件模拟实验(mini板) 问题:下面两行关于“IO方向”的代码不太明白。//IO 方向设置 #define SDA_IN() {GPIOC->CRH&=0XFFFF0FFF;GPIOC->CRH|=8<<12;} #define SDA_OUT() {GPIOC->CRH&=0XFFFF0FFF;GPIOC->CRH|=3<<12;}进过研读开发手册大概解决了这个问题。STM32 的 IO 口可以由软件配置成如下 8 种模式:1、输入浮空2、输入上拉3、输入下拉4、模拟输入5、开漏输出6、推挽输出7、推挽式复用功能8、开漏复用功能每个 IO 口可以自由编程,但 IO 口寄存器必须要按 32 位字被访问。STM32 的很多 IO 口都是 5V 兼容的,这些 IO 口在与 5V 电平的外设连接的时候很有优势,具体哪些 IO 口是 5V 兼容的,可以从该芯片的数据手册管脚描述章节查到(I/O Level 标 FT 的就是 5V 电平兼容的)。STM32 的每个 IO 端口都有 7 个寄存器来控制。他们分别是:配置模式的 2 个 32 位的端口配置寄存器 CRL 和 CRH;2 个 32 位的数据寄存器 IDR 和 ODR;1 个 32 位的置位/复位寄存器BSRR;一个 16 位的复位寄存器 BRR;1 个 32 位的锁存寄存器 LCKR;我们常用的 IO 端口寄存器只有 4 个:CRL、CRH、IDR、ODR。CRL 和 CRH 控制着每个 IO 口的模式及输出速率。STM32 的 IO 口位配置表如表所示:STM32 输出模式配置如表 :接下来我们看看端口低配置寄存器 CRL 的描述STM32 的 CRL 控制着每组 IO 端口(A~G)的低 8 位的模式。每个 IO 端口占用 CRL 的 4 个位,高两位为 CNF,低两位为 MODE。换句话说我们要控制PA0,就只要这样写:GPIOA->CRL&=0XFFFFFFF0; //GPIOA->CRL=GPIOA->CRL & 0b1111 1111 1111 1111 1111 1111 1111 0000;把PA0设置为输入就要这样写:GPIOA->CRL|=8<<0; //GPIOA->CRL=GPIOA->CRL|(0b1000)<<0;合起来控制PA0为输入就是这样写:GPIOA->CRL&=0XFFFFFFF0;GPIOA->CRL|=8<<0;PA0为输入(上/下拉)GPIOA->CRL&=0XFFFFFFF0;GPIOA->CRL|=8<<0;PA1为输入(上/下拉)GPIOA->CRL&=0XFFFFFF0F;GPIOA->CRL|=8<<4;PA2为输入(上/下拉)GPIOA->CRL&=0XFFFFF0FF;GPIOA->CRL|=8<<8;PA3为输入(上/下拉)GPIOA->CRL&=0XFFFF0FFF;GPIOA->CRL|=8<<12;PA4为输入(上/下拉)GPIOA->CRL&=0XFFF0FFFF;GPIOA->CRL|=8<<16;PA5为输入(上/下拉)GPIOA->CRL&=0XFF0FFFFF;GPIOA->CRL|=8<<20;PA6为输入(上/下拉)GPIOA->CRL&=0XF0FFFFFF;GPIOA->CRL|=8<<24;PA7为输入(上/下拉)GPIOA->CRL&=0X0FFFFFFF;GPIOA->CRL|=8<<28;同理设置为输出就是PA0为输出(通用推挽输出50MHZ)GPIOA->CRL&=0XFFFFFFF0;GPIOA->CRL|=3<<0;PA1为输出(通用推挽输出50MHZ)GPIOA->CRL&=0XFFFFFF0F;GPIOA->CRL|=3<<4;PA2为输出(通用推挽输出50MHZ)GPIOA->CRL&=0XFFFFF0FF;GPIOA->CRL|=3<<8;PA3为输出(通用推挽输出50MHZ)GPIOA->CRL&=0XFFFF0FFF;GPIOA->CRL|=3<<12;PA4为输出(通用推挽输出50MHZ)GPIOA->CRL&=0XFFF0FFFF;GPIOA->CRL|=3<<16;PA5为输出(通用推挽输出50MHZ)GPIOA->CRL&=0XFF0FFFFF;GPIOA->CRL|=3<<20;PA6为输出(通用推挽输出50MHZ)GPIOA->CRL&=0XF0FFFFFF;GPIOA->CRL|=3<<24;PA7为输出(通用推挽输出50MHZ)GPIOA->CRL&=0X0FFFFFFF;GPIOA->CRL|=3<<28;PA8为输入(上/下拉)GPIOA->CRH&=0XFFFFFFF0;GPIOA->CRH|=8<<0;PA9为输入(上/下拉)GPIOA->CRH&=0XFFFFFF0F;GPIOA->CRH|=8<<4;PA10为输入(上/下拉)GPIOA->CRH&=0XFFFFF0FF;GPIOA->CRH|=8<<8;PA11为输入(上/下拉)GPIOA->CRH&=0XFFFF0FFF;GPIOA->CRH|=8<<12;PA12为输入(上/下拉)GPIOA->CRH&=0XFFF0FFFF;GPIOA->CRH|=8<<16;PA13为输入(上/下拉)GPIOA->CRH&=0XFF0FFFFF;GPIOA->CRH|=8<<20;PA14为输入(上/下拉)GPIOA->CRH&=0XF0FFFFFF;GPIOA->CRH|=8<<24;PA15为输入(上/下拉)GPIOA->CRH&=0X0FFFFFFF;GPIOA->CRH|=8<<28;同理设置为输出就是PA8为输出(通用推挽输出50MHZ)GPIOA->CRH&=0XFFFFFF0F;GPIOA->CRH|=3<<0;PA9为输出(通用推挽输出50MHZ)GPIOA->CRH&=0XFFFFFF0F;GPIOA->CRH|=3<<4;PA10为输出(通用推挽输出50MHZ)GPIOA->CRH&=0XFFFFF0FF;GPIOA->CRH|=3<<8;PA11为输出(通用推挽输出50MHZ)GPIOA->CRH&=0XFFFF0FFF;GPIOA->CRH|=3<<12;PA12为输出(通用推挽输出50MHZ)GPIOA->CRH&=0XFFF0FFFF;GPIOA->CRH|=3<<16;PA13为输出(通用推挽输出50MHZ)GPIOA->CRL&=0XFF0FFFFF;GPIOA->CRH|=3<<20;PA14为输出(通用推挽输出50MHZ)GPIOA->CRH&=0XF0FFFFFF;GPIOA->CRH|=3<<24;PA15为输出(通用推挽输出50MHZ)GPIOA->CRH&=0X0FFFFFFF;GPIOA->CRH|=3<<28;GPIOC->CRL&=0XFFFFFFF0;GPIOx->CRL,这句话表示要操作GPIOx的低8位,就是Px0 ~ Px7GPIOx->CRH,这句话表示要操作GPIOx的高8位,就是Px8 ~ Px15所以GPIOC->CRL,这句话表示要操作GPIOC,后面的0XFFFFFFF0,表示操作PC0;0XFFFFFF0F,表示操作PC1;0XFFFFF0FF,表示操作PC2;0XFFFF0FFF,表示操作PC3;0XFFF0FFFF,表示操作PC4;0XFF0FFFFF,表示操作PC5;0XF0FFFFFF,表示操作PC3;0X0FFFFFFF,表示操作PC7;合起来的意思就是:利用“与”运算,把这个位清0,同时不影响其他的位的设置。GPIOC->CRL|=8<<0;意思就是将1000左移0位(不移位),然后再与GPIOC->CRL进行“或”运算。再根据原子的寄存器开发手册可以知道CNF0[10]、MODEO[00],对应的就是设置为上拉/输入模式。关注微信公众号:[果果小师弟],获取更多精彩内容!智果芯—服务于百万大学生和电子工程师

STM32开发---Keil中使用printf 卡死的解决办法

一、在KEIL中通过 usart + printf 输出调试信息方法1:使用 use MicroLIB(微库),在魔术棒 / Targer 选项页中勾选use MicroLIB(下面代码的13~35行不用写)方法2:不使用use MicroLIB(微库),就要加入以下全部代码, 以支持printf函数 二、使用注意问题图中的代码是写在USART的初始化文件中,并修改图中的红色下划线部分,换成你要输出的USART如果调用printf前,没USART初始化并重定义fputc, 会出现程序卡死的情况,处理办法:定义一个变量标志,变量名称随意,在完成USART初始化后,置位标志,如: char USART_IS_OK= 1;然后在fputc函数中,首行加入判断语句: if(USART_IS_OK==0) return;// 如果未完成初始化,就退出三、关于微库微库适合场景:程序快要撑爆芯片资源的情况。个人建议:尽量不用。use MicroLIB(微库)详解:如何在KEIL中使用MicroLIB关注微信公众号:[果果小师弟],获取更多精彩内容!智果芯—服务于百万大学生和电子工程师

0.96寸OLED取模教程——字符与图片取模

@[TOC]1.汉字取模1.1.打开取模软件1.2.选择字符模式1.3 设置取模方式1.4 编写代码unsigned char hanzi[][16]= //将上面生成的字模放到这里 {0x00,0x00,0x00,0xFE,0x92,0x92,0x92,0xFE,0x92,0x92,0x92,0xFE,0x00,0x00,0x00,0x00}, {0x44,0x44,0x24,0x25,0x14,0x0C,0x04,0xFF,0x04,0x0C,0x14,0x25,0x24,0x44,0x44,0x00},/*"果",0*/ {0x00,0x00,0x00,0xFE,0x92,0x92,0x92,0xFE,0x92,0x92,0x92,0xFE,0x00,0x00,0x00,0x00}, {0x44,0x44,0x24,0x25,0x14,0x0C,0x04,0xFF,0x04,0x0C,0x14,0x25,0x24,0x44,0x44,0x00},/*"果",1*/ {0x00,0x00,0x00,0xE0,0x00,0x00,0x00,0xFF,0x00,0x00,0x00,0x20,0x40,0x80,0x00,0x00}, {0x08,0x04,0x03,0x00,0x00,0x40,0x80,0x7F,0x00,0x00,0x00,0x00,0x00,0x01,0x0E,0x00},/*"小",2*/ {0x00,0xFC,0x00,0x00,0xFF,0x00,0x02,0xE2,0x22,0x22,0xFE,0x22,0x22,0xE2,0x02,0x00}, {0x00,0x87,0x40,0x30,0x0F,0x00,0x00,0x1F,0x00,0x00,0xFF,0x08,0x10,0x0F,0x00,0x00},/*"师",3*/ {0x00,0x08,0xC8,0x48,0x49,0x4E,0x48,0xF8,0x48,0x4C,0x4B,0x48,0x78,0x00,0x00,0x00}, {0x00,0x40,0x43,0x22,0x12,0x0A,0x06,0xFF,0x02,0x02,0x02,0x12,0x22,0x1E,0x00,0x00},/*"弟",4*/ };将上面的代码复制到==oledfont.h==文件中1.5 显示汉字在主函数中调用相应的函数就可以显示汉字OLED_ShowCHinese16x16(0, 0,0,Hanzi);//果(0) OLED_ShowCHinese16x16(16,0,1,Hanzi);//果(1) OLED_ShowCHinese16x16(32,0,2,Hanzi);//小(2) OLED_ShowCHinese16x16(48,0,3,Hanzi);//师(3) OLED_ShowCHinese16x16(64,0,4,Hanzi);//弟(4) 2.图片取模2.1.打开取模软件2.2.打开一张BMP图片2.3.选择图形模式2.4.生成代码方法与生成字模的代码方法相同关注微信公众号:[果果小师弟],获取更多精彩内容!智果芯—服务于百万大学生和电子工程师

PS2小车—舵机基本原理

@[TOC]  最近几年国内机器人开始起步发展,很多高校、中小学都开始进行机器人技术教学。小型的机器人、模块化的机器人、组件式的机器人是教学机器人的首选。在这些机器人产品中,舵机是很关键,使用较多的部件。根据控制方式,舵机应该称为微型伺服马达。早期在模型上使用最多,主要用于控制模型的舵面,所以俗称舵机。舵机接受一个简单的控制指令就可以自动转动到一个比较精确的角度,所以非常适合在关节型机器人产品使用。1.舵机的结构  舵机简单的说就是集成了直流电机、电机控制器和减速器等,并封装在一个便于安装的外壳里的伺服单元。能够利用简单的输入信号比较精确的转动给定角度的电机系统。舵机安装了一个电位器(或其它角度传感器)检测输出轴转动角度,控制板根据电位器的信息能比较精确的控制和保持输出轴的角度。这样的直流电机控制方式叫闭环控制,所以舵机更准确的说是伺服马达,英文servo。  舵机的主体结构如图所示,主要有几个部分:外壳、减速齿轮组、电机、电位器、控制电路。简单的工作原理是控制电路接收信号源的控制信号,并驱动电机转动;齿轮组将电机的速度成大倍数缩小,并将电机的输出扭矩放大响应倍数,然后输出;电位器和齿轮组的末级一起转动,测量舵机轴转动角度;电路板检测并根据电位器判断舵机转动角度,然后控制舵机转动到目标角度或保持在目标角度。  舵机的外壳一般是塑料的,特殊的舵机可能会有金属铝合金外壳。金属外壳能够提供更好的散热,可以让舵机里面的电机运行在更高功率下,以提供更高的扭矩输出。金属外壳也可以提供更牢固的固定位置。舵机的齿轮箱有塑料齿轮、混合齿轮、金属齿轮的差别。塑料齿轮成本底,噪音小,但强度较低;金属齿轮强度高,但成本高,在装配精度一般的情况下会有很大的噪音。小扭矩舵机、微舵、扭矩大但功率密度小的舵机一般都用塑料齿轮,如Futaba3003,辉盛的9g微舵。金属齿轮一般用于功率密度较高的舵机上,比如辉盛的MG995舵机,在和3003一样体积的情况下却能提供13KG的扭矩。Hitec甚至用钛合金作为齿轮材料,其高强度能保证3003大小的舵机能提供20几公斤的扭矩。混合齿轮在金属齿轮和塑料齿轮间做了折中,在电机输出减速箱扭矩不大的部位,用塑料齿轮。2.舵机的规格和选型2.1舵机转速  转速由舵机无负载的情况下转过60°角所需时间来衡量,常见舵机的速度一般在0.11s/60°-0.21s/60°之间。2.2 舵机扭矩  舵机扭矩的单位是KG·CM,这是一个扭矩单位。可以理解为在舵盘上距舵机轴中心水平距离1CM处,舵机能够带动的物体重量。2.3工作电压  厂商提供的速度、转矩数据和测试电压有关,在4.8V和6V两种测试电压下这两个参数有比较大的差别。如MG995在4.8V时速度为0.17秒,在6.0V时速度为0.13秒。舵机的工作电压对性能有重大的影响,舵机推荐的电压一般都是4.8V或6V。当然,有的舵机可以在7V以上工作,比如12V的舵机也不少。具体更加较高的电压可以提高电机的速度和扭矩。选择舵机还需要看我们的控制板所能提供的电压。2.4尺寸、重量和材质  舵机的功率(速度×转矩)和舵机的尺寸比值可以理解为该舵机的功率密度,一般同样品牌的舵机,功率密度大的价格高。塑料齿轮的舵机在超出极限负荷的条件下使用可能会崩齿,金属齿轮的舵机则可能会电机过热损毁或外壳变形。所以材质的选择并没有绝对的倾向,关键是将舵机使用在设计规格之内。用户一般都对金属制的物品比较信赖,齿轮箱期望选择全金属的,舵盘期望选择金属舵盘。但需要注意的是,金属齿轮箱在长时间过载下也不会损毁,最后却是电机过热损坏或外壳变形,而这样的损坏是致命的,不可修复的。塑料出轴的舵机如果使用金属舵盘是很危险的,舵盘和舵机轴在相互扭转过程中,金属舵盘不会磨损,舵机轴会在一段时间后变得光秃,导致舵机完全不能使用。综上,选择舵机需要在计算自己所需扭矩和速度,并确定使用电压的条件下,选择有150%左右甚至更大扭矩富余的舵机。3.舵机的工作原理舵机是一个微型的伺服控制系统,具体的控制原理可以用下图表示:  工作原理是控制电路接收信号源的控制脉冲,并驱动电机转动;齿轮组将电机的速度成大倍数缩小,并将电机的输出扭矩放大响应倍数,然后输出;电位器和齿轮组的末级一起转动,测量舵机轴转动角度;电路板检测并根据电位器判断舵机转动角度,然后控制舵机转动到目标角度或保持在目标角度。模拟舵机需要一个外部控制器(遥控器的接收机或者单片机)产生脉宽调制信号来告诉舵机转动角度,脉冲宽度是舵机控制器所需的编码信息。舵机的控制脉冲周期20ms,脉宽从0.5ms-2.5ms,分别对应-90度到+90度的位置(对于180°舵机)。  舵机的控制一般需要一个20ms的时基脉冲,该脉冲的高电平部分一般为0.5ms~2.5ms范围内的角度控制脉冲部分。以180度角度舵机为例,那么对应的控制关系是这样的:0.5ms--------------0度; 1.0ms------------45度; 1.5ms------------90度; 2.0ms-----------135度; 2.5ms-----------180度;如下图所示:  需要解释的是舵机原来主要用在飞机、汽车、船只模型上,作为方向舵的调节和控制装置。所以,一般的转动范围是45°、60°或者90°,这时候脉冲宽变一般只有1ms-2ms之间(比如你做一个遥控小车,用舵机控制方向,那么舵机转的角度肯定不是180度,对吧。因为你见过你开的车方向能转180度吗?)。而后舵机开始在机器人上得到大幅度的运用,转动的角度也在根据机器人关节的需要增加到-90°至90°之间,甚至还有-135°至135°之间,脉冲宽度也随之有了变化。对与机器人控制而言,我们一般通过单片机产生PWM信号控制舵机。4.STM32控制舵机代码0.5ms---------0度 0.6ms---------9度 0.7ms---------18度 0.8ms---------27度 0.9ms---------36度 1.0ms---------45度 1.1ms---------54度 1.2ms---------63度 1.3ms---------72度 1.4ms---------81度 1.5ms---------90度 1.6ms---------99度 1.7ms---------108度 1.8ms---------117度 1.9ms---------126度 2.0ms---------135度 2.1ms---------144度 2.2ms---------153度 2.3ms---------162度 2.4ms---------171度 2.5ms---------180度 \ | / \---|---/ \ | / \ | / -------------------------------------------------------------- 20ms的时基脉冲,如果想让舵机转63度,就应该发生一个高电平为1.2ms, 周期为20ms的方波,duty=1.2/20=6% ,而定时器自动重装载寄存器arr的值 为 1000 ,所以令duty=60,时占空比才为60/1000=6%. 20ms的时基脉冲,如果想让舵机转90度,就应该发生一个高电平为1.5ms, 周期为20ms的方波,duty=1.5/20=7.5% ,而定时器自动重装载寄存器arr的值 为 1000 ,所以令duty=75,时占空比才为75/1000=7.5%. 20ms的时基脉冲,如果想让舵机转126度,就应该发生一个高电平为1.9ms, 周期为20ms的方波,duty=1.9/20=9.5% ,而定时器自动重装载寄存器arr的值 为 1000 ,所以令duty=95,时占空比才为95/1000=9.5%. ----------------------------------------------------------------- void SERVO_Init(void) GPIO_InitTypeDef GPIO_InitStruct; TIM_TimeBaseInitTypeDef TIM_TimeStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AF_PP;//配置为复用推挽输出 GPIO_InitStruct.GPIO_Pin=GPIO_Pin_7; GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct); TIM_TimeStructure.TIM_Period=1000;//1000 自动重装载寄存器的值,周期为50 000Hz/1000=50Hz,即输出PWM波形的频率为20ms。 TIM_TimeStructure.TIM_Prescaler=1440-1;;// 1400 时钟预分频系数为3600,72 000 000Hz/1400=50000Hz =50KHZ。 TIM_TimeStructure.TIM_ClockDivision=TIM_CKD_DIV1; TIM_TimeStructure.TIM_CounterMode=TIM_CounterMode_Up; TIM_TimeStructure.TIM_RepetitionCounter=0; TIM_TimeBaseInit(TIM3,&TIM_TimeStructure); TIM_ARRPreloadConfig(TIM3,ENABLE); //使能ARR预装载寄存器(影子寄存器) TIM_OCInitStructure.TIM_OCMode=TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState=TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse=0;//占空比大小 TIM_OCInitStructure.TIM_OCPolarity=TIM_OCPolarity_High; TIM_OC2Init(TIM3,&TIM_OCInitStructure); TIM_OC2PreloadConfig(TIM3,TIM_OCPreload_Enable); TIM_Cmd(TIM3,ENABLE); TIM_CtrlPWMOutputs(TIM3,ENABLE); //舵机角度控制 void SERVO_Angle_Control(uint16_t Compare2) TIM_SetCompare2(TIM3,Compare2);//设置通道2为可变的pwm }配置号上面的程序,如果你想让舵机旋转90度,只需要在你程序的某个位置放上这句话就可以了SERVO_Angle_Control(75);//舵机旋转90度  原因就是20ms的时基脉冲,如果想让舵机转90度,就应该发生一个高电平为1.5ms,周期为20ms的方波,duty=1.5/20=7.5% ,而定时器自动重装载寄存器arr的值为 1000 ,所以令duty=75,时占空比才为75/1000=7.5%。以此类推,你想让舵机转多大的角度按照这个方法设置就行了。  获取更多开源项目,请关注微信公众号:果果小师弟。关注微信公众号:[果果小师弟],获取更多精彩内容!智果芯—服务于百万大学生和电子工程师

平衡小车—编码器使用教程与测速原理

@[TOC]来自平衡小车之家,与大家一起学习编码器使用与测速原理。1.编码器概述  编码器是一种将角位移或者角速度转换成一连串电数字脉冲的旋转式传感器,我们可以通过编码器测量到底位移或者速度信息。编码器从输出数据类型上分,可以分为增量式编码器和绝对式编码器。  从编码器检测原理上来分,还可以分为光学式、磁式、感应式、电容式。常见的是光电编码器(光学式)和霍尔编码器(磁式)。2.编码器原理  光电编码器是一种通过光电转换将输出轴上的机械几何位移量转换成脉冲或数字量的传感器。光电编码器是由光码盘和光电检测装置组成。光码盘是在一定直径的圆板上等分地开通若干个长方形孔。由于光电码盘与电动机同轴,电动机旋转时,检测装置检测输出若干脉冲信号,为判断转向,一般输出两组存在一定相位差的方波信号。  霍尔编码器是一种通过磁电转换将输出轴上的机械几何位移量转换成脉冲或数字量的传感器。霍尔编码器是由霍尔码盘和霍尔元件组成。霍尔码盘是在一定直径的圆板上等分地布置有不同的磁极。霍尔码盘与电动机同轴,电动机旋转时,霍尔元件检测输出若干脉冲信号,为判断转向,一般输出两组存在一定相位差的方波信号。  可以看到两种原理的编码器目的都是获取AB相输出的方波信号,其使用方法也是一样,下面是一个简单的示意图。[video(video-8tavfOQZ-1595665366556)(type-bilibili)(url-https://player.bilibili.com/player.html?aid=52264565)(image-https://ss.csdn.net/p?http://i0.hdslb.com/bfs/archive/43393f4d61501072137d54b0fe5c2ce6a2d5d162.jpg)(title-讲下增量型旋转编码器的工作原理,再拆开一个编码器看看内部构造)]3.编码器接线说明  具体到我们的编码器电机,我们可以看看电机编码器的实物。  这是一款增量式输出的霍尔编码器。编码器有AB相输出,所以不仅可以测速,还可以辨别转向。根据上图的接线说明可以看到,我们只需给编码器电源5V供电,在电机转动的时候即可通过AB相输出方波信号。编码器自带了上拉电阻,所以无需外部上拉,可以直接连接到单片机IO读取。4.编码器软件四倍频技术  下面我们说一下编码器倍频的原理。为了提高大家下面学习的兴趣,我们先明确,这是一项实用的技术,可以真正地把编码器的精度提升4倍。作用可类比于单反相机的光学变焦,而并非牺牲清晰度来放大图像的数码变焦。  0K,先看看下面编码器输出的波形图。  这里,我们是通过软件的方法实现四倍频。首先可以看到上图编码器输出的AB相波形,正常情况下我们使用M法测速的时候,会通过测量单位时间内A相输出的脉冲数来得到速度信息。常规的方法,我们只测量A相(或B相)的上升沿或者下降沿,也就是上图中对应的数字1234中的某一个,这样就只能计数3次。而四倍频的方法是测量A相和B相编码器的上升沿和下降沿。这样在同样的时间内,可以计数12次(3个1234的循环)。这就是软件四倍频的原理。**************************************************************************/ /************************************************************************** 函数功能:把TIM2初始化为编码器接口模式 入口参数:无 返回 值:无 **************************************************************************/ void Encoder_Init_TIM2(void) TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);//使能定时器2的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//使能PB端口时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1; //端口配置 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入 GPIO_Init(GPIOA, &GPIO_InitStructure); //根据设定参数初始GPIOB TIM_TimeBaseStructInit(&TIM_TimeBaseStructure); TIM_TimeBaseStructure.TIM_Prescaler = 0x0; // 预分频器 TIM_TimeBaseStructure.TIM_Period = ENCODER_TIM_PERIOD; //设定计数器自动重装值 TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;//选择时钟分频:不分频 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;////TIM向上计数 TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);//使用编码器模式3,模式3就我们在这里所说的4倍频,详细信息查看stm32f1技术手册 TIM_ICStructInit(&TIM_ICInitStructure); TIM_ICInitStructure.TIM_ICFilter = 10; TIM_ICInit(TIM2, &TIM_ICInitStructure); TIM_ClearFlag(TIM2, TIM_FLAG_Update);//清除TIM的更新标志位 TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); //Reset counter TIM_SetCounter(TIM2,0); TIM_Cmd(TIM2, ENABLE); /************************************************************************** 函数功能:把TIM4初始化为编码器接口模式 和TIM2同理 入口参数:无 返回 值:无 **************************************************************************/ void Encoder_Init_TIM4(void) TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);//使能定时器4的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//使能PB端口时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7; //端口配置 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入 GPIO_Init(GPIOB, &GPIO_InitStructure); //根据设定参数初始化GPIOB TIM_TimeBaseStructInit(&TIM_TimeBaseStructure); TIM_TimeBaseStructure.TIM_Prescaler = 0x0; // 预分频器 TIM_TimeBaseStructure.TIM_Period = ENCODER_TIM_PERIOD; //设定计数器自动重装值 TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;//选择时钟分频:不分频 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;////TIM向上计数 TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure); TIM_EncoderInterfaceConfig(TIM4, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);//使用编码器模式3 TIM_ICStructInit(&TIM_ICInitStructure); TIM_ICInitStructure.TIM_ICFilter = 10; TIM_ICInit(TIM4, &TIM_ICInitStructure); TIM_ClearFlag(TIM4, TIM_FLAG_Update);//清除TIM的更新标志位 TIM_ITConfig(TIM4, TIM_IT_Update, ENABLE); //Reset counter TIM_SetCounter(TIM4,0); TIM_Cmd(TIM4, ENABLE); }5.单片机如何采集编码器数据  因为编码器输出的是标准的方波,所以我们可以使用单片机(STM32\STM851等)直接读取。在软件中的处理方法是分两种,自带编码器接口的单片机如STM32,可以直接使用硬件计数。而没有编码器接口的单片机如51单片机,可以通过外部中断读取,比如把编码器A相输出接到单片机的外部中断输入口,这样就可通过跳变沿触发中断,然后在对应的外部中断服务函数里面,通过B相的电平来确定正反转。如当A相来一个跳变沿的时候,如果B相是高电平就认为是正转,低电平就认为是反转。/************************************************************************** 函数功能:单位时间读取编码器计数 入口参数:定时器 返回 值:速度值 **************************************************************************/ int Read_Encoder(u8 TIMX) int Encoder_TIM; switch(TIMX) case 2: Encoder_TIM= (short)TIM2 -> CNT; TIM2 -> CNT=0;break; case 3: Encoder_TIM= (short)TIM3 -> CNT; TIM3 -> CNT=0;break; case 4: Encoder_TIM= (short)TIM4 -> CNT; TIM4 -> CNT=0;break; default: Encoder_TIM=0; return Encoder_TIM; /************************************************************************** 函数功能:TIM4中断服务函数 入口参数:无 返回 值:无 **************************************************************************/ void TIM4_IRQHandler(void) if(TIM4->SR&0X0001)//溢出中断 TIM4->SR&=~(1<<0);//清除中断标志位 /************************************************************************** 函数功能:TIM2中断服务函数 入口参数:无 返回 值:无 **************************************************************************/ void TIM2_IRQHandler(void) if(TIM2->SR&0X0001)//溢出中断 TIM2->SR&=~(1<<0);//清除中断标志位 }6.获取方式  测试编码器代码获取方式,关注微信公众号:果果小师弟,后套回复:编码器。即可获取STM32测试编码器代码。关注微信公众号:[果果小师弟],获取更多精彩内容!智果芯—服务于百万大学生和电子工程师

平衡小车—TB6612FNG与直流电机控制教程

@[TOC]这篇教程来自平衡小车之家,与大家一起学习直流电机的原理和控制、减速器的作用,并介绍一款直流电机驱动芯片TB6612FNG。1.直流电机原理  下面是分析直流电机的物理模型图。其中,固定部分有磁铁,这里称作主磁极;固定部分还有电刷。转动部分有环形铁心和绕在环形铁心上的绕组。(其中2个小圆圈是为了方便表示该位置上的导体电势或电流的方向而设置的)  它的固定部分(定子)上,装设了一对直流励磁的静止的主磁极N和S,在旋转部分(转子)上装设电枢铁心。在电枢铁心上放置了两根导体连成的电枢线圈,线圈的首端和末端分别连到两个圆弧形的铜片上,此铜片称为换向片。换向片之间互相绝缘,由换向片构成的整体称为换向器。换向器固定在转轴上,换向片与转轴之间亦互相绝缘。在换向片上放置着一对固定不动的电刷B1和B2,当电枢旋转时,电枢线圈通过换向片和电刷与外电路接通。  在电刷上施加直流电压U,电枢线圈中的电流流向为:N极下的有效边中的电流总是一个方向,而S极下的有效边中的电流总是另一个方向。这样两个有效边所受的洛伦兹力的方向一致(可以根据左手法则判定),电枢开始转动。  具体来说就是,把上图中的+和-分别接到电池的正极和负极,电机即可转动;如果是把上图中的+和-分别接到电池的负极和正极,则电机会反方向转动。电机的转速可以理解为和外接的电压是正相关的(实际是由电枢电流决定)。  总而言之,如果我们可以调节施加在电机上面的直流电压大小,即可实现直流电机调速,改变施加电机上面直流电压的极性,即可实现电机换向。2.减速器  一般直流电机的转速都是一分钟几千上万转的,所以一般需要安装减速器。减速器是一种相对精密的机械零件,使用它的目的是降低转速,增加转矩。减速后的直流电机力矩增大、可控性更强。按照传动级数不同可分为单级和多级减速器;按照传动类型可分为齿轮减速器、蜗杆减速器和行星齿轮减速器。齿轮减速箱体积较小,传递扭矩大,但是有一定的回程间隙。  蜗轮蜗杆减速机的主要特点是具有反向自锁功能,可以有较大的减速比,但是一般体积较大,传动效率不高,精度不高。行星减速机其优点是结构比较紧凑,回程间隙小、精度较高,使用寿命很长,额定输出扭矩可以做的很大,但价格略贵。3.电机实物接线图解具体到我们的电机,我们可以看看电机后面的图解。  上面介绍了一大堆说直流电机只引出两个线,怎么这个电机有 6个线,而且还有两个大焊点呢?其实,根据上面的图解也知道,那两个焊点分别和黄线和棕线是连接在一起的。也就是说只有6 个线,而6P 排线中,中间的四根线(红绿白黑)是编码器的线,只是用于测速,和直流电机本身没有联系。我们在实现开环控制的时候无需使用。  综上所述,我们只需控制施加在黄线和棕色线两端的直流电压大小和极性即可实现调试和换向。4.TB6612FNG使用说明  要实现上面的调试和换向功能,我们可以使用单片机实现的,但是单片机IO的带负载能力较弱,而直流电机是大电流感性负载,所以我们需要功率放大器件,在这里,我们选择了TB6612FNG。  TB6612FNG是东芝半导体公司生产的一款直流电机驱动器件,它具有大电流MOSFET-H桥结构,双通道电路输出,可同时驱动2个电机。也许大家更熟悉L298N,其实这两者的使用基本一致的。而且,相比 L298N的热耗性和外围二极管续流电路,它无需外加散热片,外围电路简单,只需外接电源滤波电容就可以直接驱动电机,利于减小系统尺寸。对于 PWM信号输入频率范围,高达100 kHz的频率更是足以满足我们大部分的需求了。  以下是TB6612FNG 的主要参数:  最大输入电压:VM = 15V  最大输出电流:Iout = 1.2A(平均/3.2A(峰值)  正反转/短路刹车/停机功能模式  内置过热保护和低压检测电路  以下是TB6612 模块测试一个电机的接线图:  VM直接接电池即可,VCC 是内部的逻辑供电,一般给3.3 或者5V 都行,模块的3 个GND 接任意一个即可,因为都是导通的,STBY置高模块才能正常工作。  完成上面的接线之后,我们就可以开始控制电机了,上图中红色部分的5个引脚控制一路电机,蓝色部分的控制另外一路电机,这里只讲其中的A 路,B路的使用是一样的。AO1 和AO2 分别接到电机的+和-。然后通过PWMA、AIN2、AIN1控制电机。其中PWMA 接到单片机的PWM 引脚,一般10Khz 的PWM 即可,并通过改变占空比来调节电机的速度。下面是真值表:  如果大家手头上没有单片机的话,一样可以测试的,直接接电源的引脚即可。   AIN1接 3.3~5V、 AIN2 接GND、 PWMA接 3.3 ~5V。这样相当于控制电机满占空比正转;  AIN1接 GND、 AIN2 接3.3~ 5V、 PWMA接 3.3~5V。这样相当于控制电机满占空比反转;5.TB6612FNG原理图与PCB6.获取方式  原理图和PCB获取方式,关注微信公众号:果果小师弟,后套回复:TB6612。即可获取PCB工程文件。关注微信公众号:[果果小师弟],获取更多精彩内容!智果芯—服务于百万大学生和电子工程师

2019年全国大学生电子设计大赛(简单电路特性测试仪)

@TOC摘 要:本测试仪以低功耗STM32F407单片机为核心控制器件,利用DDS芯片AD9850和电阻分压电路产生1kHz 30mV正弦信号输入到待测电路,通过采集输入电压Ui和流过输入电阻上电流Ii,算出输入电阻Ri;采集开路电压Uopen和带载电压U1并结合流过负载电阻上的电流Io,算出输出电阻Ro;结合待测电路的工作特性来分析和判断待测电路的故障原因。  实测结果表明,该测试仪能正常输出1KH的正弦波,测得输入电阻为3.2K ohm,输出电阻为1.96K ohm,在输入1KHz正弦信号下,增益为20.3dB,扫描频率可清晰显示该放大电路的幅频特性曲线且测得上限频率为65KHz;改变放大器的参数,该测试仪能准确判断并分析故障原因。该测试仪基本满足基础部分要求和发挥部分要求。关键字:AD9850;STM32F407;电路特性测试仪;GUI一、方案设计与论证1、控制方案选择  方案一:单片机+FPGA方式。该方案利用FPGA输出1KHz正弦波信号,利用单片机控制信号源以及控制FPGA,即由单片机对输出信号进行采集、处理、计算、显示和人机交互接面等顶层功能,由FPGA完成1KHz正弦波信号输出。  方案二:采用基于ARM Cortex™-M4内核的STM32F407系列单片机。该单片机有着高主频,内部集成FPU有很强的计算能力。能够处理复杂计算和控制。考虑其可行性、单片机所用的利用资源后,采用方案二作为本次设计方案。2、正弦波信号生成方案选择  方案一:利用STM32单片机的DAC产生正弦波。STM32F407的DAC模块配置为12位模式,与DMA控制器配合使用。但是需要频率变化,通过单片机产生正弦波高频段其精度不够且占资源。  方案二:利用SPWM逆变器,输出正弦波,虽然频率和频率波形可控制,但硬件电路较为复杂需要额外的程序才能实现。  方案三:利用ADI公司应用的最广泛的DDS芯片AD9850制作正弦波发生器,此模块可以输出高稳定性的正弦波且频率范围可达40MHz,幅度范围为80mV~2V。另外此芯片采用专用的稳压基准芯片。供电更加稳定。  综上所述,采用第三种方案作为本次设计方案。3、总体方案设计  系统总体框图如图1所示。本系统采用双核处理,采用基于ARM内核的STM32F103单片机软件控制DDS模块AD9850产生频率为1KHz幅值为1.0V的正弦波信号,通过电阻分压到50mv输入放大电路 。LM358对输入小信号进行放大并让单片机采样。通过测量内阻前后的电压,根据 计算出输入电路的电流,根据输入电压测量被测电路的电压计算出。采用STM32F4单片机采样电路各个模块的电压,进行内部FFT,计算出各个器件的电阻、电压值。 二、理论分析计算与故障分析1、理论输入电阻与输出电阻的计算被测电路如图2 所示单极共射放大电路,三极管三个极电压和电流决定。 根据共设放大电路的交流小信号等效模型,可知输入电阻为 Ri=R1//R2//rbe输出电阻为 R0=R3通过测量后得到输入电阻Ri=3K,输出电阻为2K。2、输入电压对三极管工作状态的影响  (1)、由仿真可知,当三极管输入电压低于20mv,大于80mv时,电路发生失真,三极管输入电压应为20~80mv。  (2)、输入电压为30mv时,幅频特性曲线如图2.2所示,下限截止频率为77Hz,上限截止频率为110 Khz。2、故障分析  故障1、 R1开路,三极管工作于截止区,此时输出直流电压为电源电压12V,当R1短路时,输入电阻减小。  故障2、 R2开路,三极管工作在饱和区,此时三极管导通压降较小,输出接近电源电压12V,当R2短路时,输入电阻减小。  故障3、当R3开路,三极管工作在饱和区,输出电压为零,输出电阻较大,当R3短路时,三极管工作在放大区,输出电压接近电源电压,输出电阻较小。  故障4、当R4开路时,三极管工作在截止区,输出电压为电源电压,输出电阻趋近无穷大,当R4短路时,输入电阻变小,输出电压为0。  故障5、当C1开路时,输入为零,输出交流信号为0,当C1为原来两倍,下限截止频率减小。  故障6、当C2开路,输出增益减小,输出电压减小,当C2为原来的两倍时,输出增益增大,输出电压增大。  故障7、当C3开路,上限截止频率增大,当C3为原来的两倍时,上限截止频率减小。三、电路与程序1、总体电路设计2、实际输入电阻的计算与测量2.1、DDS分压1kHz正弦波电路设计  为了保证放大电路输出波形不失真,需保证电路特性测试仪输出的1kHz正弦信号的幅值为10mv-30mv之间,但DDS模块最小输出的幅值为80mv。故需要将DDS模块将行电阻分压,使幅值降到放大电路输出波形不失真的临界情况。分压电阻的电路如图3.2.1所示,其中: 2.2.DDS分压1kHz正弦波电路设计  本设计需要测量输入电压,DDS模块输入的电压峰值为30mv,为使其能更好的被主控部分采集处理,需要通过一个同相比例放大器将输入电压放大至单片机可以采集的电压。放大器采用LM358双运算放大器,放大倍数 β=1+R2/R1。3、输出电阻测量电路  输出电阻的测量同输入电阻的测量方法基本原理相同,需要测量电路的输出电压,当输出不接负载时三极管放大电路的输出电压为Uo,当接入负载时输出电压U1,由于输出电阻为高阻抗,输出电流非常小,故需要对输出电路进行电阻分压和电压跟随。且分压电阻需要设计为非常大,原因是电阻大电流就会非常小,那么相当于断路,故在进行电压采样时就不会对放大器前级电路产生影响或者说产生的影响非常小。在电压采样的后面在加上一个相当于电压表内阻的理论计算电阻Rs和一个开关管,通过控制开关管的通断使输出电路处于开路与断路状态,就可以通过计算开路与断路状态下的电压车和计算电阻Rs,计算出输出电流,从而计算三极管放大电路的出输出电阻。开关管选用N沟道功率MOSFET管IRF7843,它具有开关速度快、导通电阻小、栅极电容小和无二次击穿等显著特点。可以满足开关管要求。如图7所示: 4、程序框图  程序的系统框图如图8所示:系统初始化后,开始采集各路电压,并进行分析然后自动测量,通过扫频绘制幅频特性曲线,最终在显示屏上显示。 四、测试结果与分析1测试仪器2测试结果及分析4.1、输入电阻测试通过测试输入电阻满足题目要求误差精度,达到要求。4.2、输出电阻测试通过测试输出电阻满足题目要求误差精度,达到了要求。4.3、幅频特性测试通过测试测得上限截止频率为65Khz,用示波器测得上限截止频率为66.7Khz,误差小于25%,并显示在屏幕上,完成了题目要求,则是幅频特性曲线如图9所示。4.4、故障分析测试  通过将被测电路中的电阻开路和短路,电容开路,增大电容倍数,测试仪能全自动的检测出故障并显示出原因,完成了发挥部分的题目,达到了题目的要求。测试数据表如表9所示:关注微信公众号:[果果小师弟],获取更多精彩内容!智果芯—服务于百万大学生和电子工程师

基于STM32的MLX90614人体红外测温枪

@[TOC]  今天分享一个项目是做一个红外测温的。这个东西网上都有现成的资料和代码,做起来不难。关于红外测温用的芯片是mlx90614。很巧的是“芯知识学堂”已经将他们的红外测温枪方案全部开源了出来。如果大家想自己做一个红外测温枪可以去看看他们的资料,自己尝试做着玩一下。  那么在这里我就来写一写关于mlx90614红外测温的驱动代码和来说一说他的原理。主要是利用STM32F103C8T6单片机和mlx90614模块将测量的温度显示到0.96寸oled上面,如果你没有oled的话也可以用串口调试助手把温度信息打印出来。一、所需材料STM32F103C8T6最小系统板+mlx90614红外测温模块+0.96寸oled+一个jlink下载器。二、mlx90614传感器介绍  MLX90614 系列模块是一组通用的红外测温模块。在出厂前该模块已进行校验及线性化,具有非接触、体积小、精度高,成本低等优点。被测目标温度和环境温度能通过单通道输出,并有两种输出接口,适合于汽车空调、室内暖气、家用电器、手持设备以及医疗设备应用等。测温方式可分为接触式和非接触式,接触式测温只能测量被测物体与测温传感器达到热平衡后的温度,所以响应时间长,且极易受环境温度的影响;而红外测温是根据被测物体的红外辐射能量来确定物体的温度,不与被测物体接触,具有影响动被测物体温度分布场,温度分辨率高、响应速度快、测温范围广、不受测温上限的限制、稳定性好等特点,所以我们选择mlx90614来作为红外测温模块。  单片机与mlx90614红外测温模块之间通信的方式是“类IIC”通信,意思就是通信方式跟IIC通信方式很像但又不是IIC,它有另外一个名字叫做SMBus。SMBus (System Management Bus)是 1995 年由 intel 公司提出的一种高效同步串行总线,SMBus 只有两根信号线:双向数据线和时钟信号线,容许 CPU 与各种外围接口器件以串行方式进行通信、交换信息,即可以提高传输速度也可以减小器件的资源占用,另外即使在没有SMBus 接口的单片机上也可利用软件进行模拟。三、MLX90614工作原理  MLX90614有MLX81101红外热电堆传感器和包括含有稳压电路、低噪声放大器、A/D转换器、DSP单元、脉宽调制电路及逻辑控制电路的MLX90302信号处理芯片构成。  其工作原理为:红外热电堆传感器输出的温度信号经过内部低噪声、低失调的运算放大器(OPA)放大后经过A/D转换器(ADC)转换为17位数字信号通过可编程FIR及IIR低通数字滤波器(即DSP)处理后输出,输出结果存储在其内部RAM存储单元中。  MLX90614中有两个存储器,分别为EEPROM和RAM。  MLX90614中共有32个字长为16位的EEPROM存储单元,其地址为000H—01FH。  EEPROM中所有的寄存器都是可以通过SMBus进行读取,但只有部分寄存器是可以进行改写的(地址为0x00, 0x01, 0x02, 0x03, 0x04, 0x05*,0x0E, 0x0F, 0x09)。可改写部分如左表所示。  Tomax和Tomin是设定的测量物体温度上、下限,Ta范围即环境温度范围。  其测量温度上限计算方法为:Tomax=100×(To MAX+273.15),通过计算将结果写入000H;温度下限计算方法与上限计算方法一样,将计算结果写入001H。  MLX90614中总共有32个17位的RAM存储单元,用户不能通过RAM来写入数据,只能读取RAM中的部分存储单元读取16位存储数据。其采集的环境温度数据保存在地址006H存储单元中,采集的被测物体温度数据存储在007H存储单元中。因此运用存储在RAM地址中的数据,通过公式的计算,可以得到环境温度Ta及被测物体温度数据To。   Ta和To既可通过SMBus读取RAM单元(分辨率0.02°C ,固定范围)输出,也可通过PWM数字模式输出(10位分辨率,范围可配置)。由于变电所测温温度范围与MLX90614出厂时校准的温度范围符合,因此可直接采用SMBus方式进行温度数据Ta和To的读取输出。  Ta=RAM(006H)x0.02-273.15   To=RAM(007H)x0.02-273.15四、IIC协议原理IIC主从机之间通讯步骤如下:1.主机发送一个起始信号通知各从机就位。2.主机发送从机地址和读写标志位(写标志位为0,读标志位为1)从机地址和读写标志位一共占用8位,地址占用高7位,读写标志位占用最低位,3从机给主机回复响应(ACK)4.如果是写模式,主机发送一字节数据等待从机响应,主机收到响应之后如果还有数据要发就继续发送第二段数据等待响应……直到发送完成;如果是读模式,此时主机STM32读取从机发来的数据,并给从机响应,如果从机还有数据要发送(接着汇报第二段),主机接着读取然后发送响应给从机…5.主机给从机一个停止信号1.写时序  首先主机发送一个起始位,然后在发送从机的地址0x00和写标志位0,一共是8位。发送这8位之后,主机等待从机的响应,如过从机发送的是应答信号,主机就继续向从机发送一个字节的数据,同理再一次等待从机的响应,主机收到响应之后如果还有数据要发就继续发送第二段数据等待响应…直到发送完成;写所用到的函数有:1.void SMBus_StartBit(void);----主机发送一个起始位u8 SMBus_ SendByte (u8 ack_nack)--- 主机发送从机的地址和写标志位SMBus_ReceiveBit(ack_nack);-- 从机发送的是应答信号u8 SMBus_SendByte(u8 ack_nack)—主机发送从机的一个字节的数据5.void SMBus_StopBit(void)-- ----主机发送一个停止位2.读时序  首先主机发送一个起始位,然后在发送从机的地址0x00和读标志位1,一共是8位。发送这8位之后,主机等待从机的响应,如过从机发送的是应答信号,从机就向主机发送一个字节的数据。读时序就是主机从从机读数据,反过来就是从机向主机发送数据,同理,从机在发送完一个字节的数据之后也要向主机询问是否继续发送,如果主机让继续发送也就是发送了应答信号,那么从机就继续发送数据,每次发送完之后都要询问是否要继续发送,直到主机发送了非应答信号,从机才停止发送数据,最后主机再发送一个停止信号即可。读所用到的函数有:1.void SMBus_StartBit(void);---主机发送一个起始位u8 SMBus_ SendByte (u8 ack_nack)—主机发送从机的地址和写标志位SMBus_ReceiveBit(ack_nack);-- 从机发送的是应答信号u8 SMBus_ReceiveByte (u8 ack_nack)--从机向主机发送的一个字节的数据5.void SMBus_StopBit(void)-- ----主机发送一个停止位3.通信过程1.起始信号--在时钟线SCL高电平期间数据线SDA发生下降沿跳变产生起始信号2.应答信号--在时钟线SCL为高电平期间数据线SDA保持低电平为应答信号3.非应答信号--在时钟线SCL为高电平期间数据线SDA保持高电平为非应答信号4.结束信号--在时钟线SCL高电平期间数据线SDA发生上升沿跳变产生停止信号5.数据信号--在数据传输期间,时钟线 SCL 为高电平期间,如果数据线 SDA 为高电平则代表二进制1,同理,时钟线 SCL 为高电平期间,如果数据线 SDA 为低电平则代表二进制 0。6.上面1号框是SDA 数据有效期,2号框是数据改变期。五、程序编写1.起始信号与停止信号void SMBus_StartBit(void) SMBUS_SDA_H(); // 首先拉高数据线 SMBus_Delay(5); // 延时几微妙 SMBUS_SCK_H(); // 拉高时钟线 SMBus_Delay(5); // 延时几微妙 SMBUS_SDA_L(); // 拉低数据线 SMBus_Delay(5); // 延时几微妙 //在SCK=1时,检测到SDA由1到0表示通信开始(下降沿) SMBUS_SCK_L(); // 拉低时钟线 SMBus_Delay(5); // 延时几微妙 void SMBus_StopBit(void) SMBUS_SCK_L(); // 拉低时钟线 SMBus_Delay(5); // 延时几微妙 SMBUS_SDA_L(); // 拉低数据线 SMBus_Delay(5); // 延时几微妙 SMBUS_SCK_H(); // 拉高时钟线 SMBus_Delay(5); // 延时几微妙 SMBUS_SDA_H(); // 拉高数据线 }2.发送一个字节u8 SMBus_SendByte(u8 Tx_buffer) u8 Bit_counter; u8 Ack_bit; u8 bit_out; for(Bit_counter=8; Bit_counter; Bit_counter--) if (Tx_buffer&0x80)//如果最高位为1 bit_out=1; // 把最高位置1 else //如果最高位为0 bit_out=0; // 把最高位置0 SMBus_SendBit(bit_out); // 把最高位发送出去 Tx_buffer<<=1;// 左移一位把最高位移出去等待下一个最高位,循环8次,每次都发最高位,就可把一个字节发出去了 Ack_bit=SMBus_ReceiveBit(); // Get acknowledgment bit return Ack_bit; }  发送一个字节也就是8个bit位,我们的做法就是循环8次,每次将最高位发送出去。如果最高位为1,我们将这一个字节Tx_buffer和0x80(10000000)进行"与运算",把最高位置为1。如果最高位为0,就把最高位置为0,再通过 SMBus_SendBit(bit_out); 把最高位发送出去,之后再通过Tx_buffer<<=1;左移一位把最高位移出去等待下一个最高位,循环8次,每次都发最高位,就可把一个字节发出去了。这里是SMBus发送一个字节,我们知道在从机发送完一个字节之后,主机要返回一个应答信号告诉从机是否继续发送下一个字节,所以这里我们 使用Ack_bit=SMBus_ReceiveBit();这条语句来告诉从机书否还要继续发送下一个字节,如果返回的是0就继续发送,如果返回的是1就停止发送。3.接收一个字节u8 SMBus_ReceiveByte(u8 ack_nack) u8 RX_buffer; u8 Bit_Counter; for(Bit_Counter=8; Bit_Counter; Bit_Counter--) if(SMBus_ReceiveBit())// Get a bit from the SDA line RX_buffer <<= 1;// If the bit is HIGH save 1 in RX_buffer RX_buffer |=0x01;//如果Ack_bit=1,把收到应答信号1与0000 0001 进行或运算,确保为1 RX_buffer <<= 1;// If the bit is LOW save 0 in RX_buffer RX_buffer &=0xfe;//如果Ack_bit=1,把收到应答信号0与1111 1110 进行与运算,确保为0 SMBus_SendBit(ack_nack);//把应答信号发出去,如果0,就进行下一次通信,如果为1,就拜拜了。 return RX_buffer; }  从SMBus上接收一个字节也就是8位,我们也是用一个for循环将8各一次接受过来。首先问我们通过if函数来判断SMBus_ReceiveBit()是否是收到了应答信号,如果收到了的应答信号也就是1,我们就将收到的数据左移1位RX_buffer <<= 1;如果左移一位之后空出的那一位数据位1然后就与0x01也就是0000 0001 进行"或运算",确保为1。如果左移一位之后空出的那一位数据位0然后就与0xfe也就是1111 1110 进行"或运算",确保为0。最后SMBus把应答信号发出去,如果0,就进行下一次通信,如果为1,就拜拜了,就不接受数据了。4.数据校验u8 PEC_Calculation(u8 pec[]) u8 crc[6];//存放多项式 u8 BitPosition=47;//存放所有数据最高位,6*8=48 最高位就是47位 u8 shift; u8 i; u8 j; u8 temp; { //Load pattern value 0x00 00 00 00 01 07 crc[5]=0; crc[4]=0; crc[3]=0; crc[2]=0; crc[1]=0x01; crc[0]=0x07; BitPosition=47; shift=0; while((pec[i]&(0x80>>j))==0 && i>0) BitPosition--; if(j<7) j=0x00; shift=BitPosition-8; while(shift) for(i=5; i<0xFF; i--) if((crc[i-1]&0x80) && (i>0)) temp=1; temp=0; crc[i]<<=1; crc[i]+=temp; shift--; for(i=0; i<=5; i++) pec[i] ^=crc[i]; while(BitPosition>8); return pec[0]; }  这个数据校验是很重要的,目的就是来判断检测你采集的数据是否是是正确的。  也就是我们程序会把PEC的数据通过SA_W、Command、SA_R把LSByte、MSByte读出来。然后我们自己再写一个校验的函数来判断读到的PEC数据是否正确,如果正确了才能进行下一步的判断,如果不正确的话就继续读,直到正确为止。如何才能知道自己读到的数据是否正确呢?这就需要自己单独建立一个C工程,把这个函数u8 PEC_Calculation(u8 pec[])放到里面运行一遍,看看自己输入的数据时候跟自己要得到数据数据是否一样就可以了。也就是把0x00、0x3a、0xd2、0xb5、0x07、0xb4放到这个函数运行,最终返回出来的数据是否是0x30就可以了,如果是就说明数据校验正确,如果不是就需要该函数,至于为啥返回的是0x30请看上面的时序表。5.读取温度函数u16 SMBus_ReadMemory(u8 slaveAddress, u8 command) u16 data; u8 Pec; u8 DataL=0; u8 DataH=0; u8 arr[6]; u8 PecReg; u8 ErrorCounter; ErrorCounter=0x00;// Initialising of ErrorCounter slaveAddress <<= 1; //2-7位表示从机地址 从机地址左移一位,把读写位空出来 repeat: SMBus_StopBit(); --ErrorCounter; if(!ErrorCounter) //ErrorCounter=0? break; //如果为0就跳出do-while{}循环 SMBus_StartBit(); if(SMBus_SendByte(slaveAddress))//发送从机地址最低位Wr=0表示接下来写命令 goto repeat; if(SMBus_SendByte(command))//发送命令 goto repeat; SMBus_StartBit(); if(SMBus_SendByte(slaveAddress+1)) //发送从机地址+1最低位Rd=1表示接下来读数据 goto repeat; DataL = SMBus_ReceiveByte(ACK); //读低位数据保存到DataL DataH = SMBus_ReceiveByte(ACK); //读高位数据保存到DataH Pec = SMBus_ReceiveByte(NACK); //读校验数据保存到Pec SMBus_StopBit(); arr[5] = slaveAddress; arr[4] = command; arr[3] = slaveAddress+1; arr[2] = DataL; arr[1] = DataH; arr[0] = 0; PecReg=PEC_Calculation(arr);//Calculate CRC 数据校验 while(PecReg != Pec); data = (DataH<<8) | DataL; return data; }  这个函数的输入参数就IIC设备的从机地址和command寄存器地址。首先从机地址左移一位,把读写位空出来,因为不知道最开始是啥状态,我们就需要发送一个停止信号,因为ErrorCounter=0x00;那么进行"--"操作之后就不等于0,就进行下面的操作。接下来就发送起始信号,发送从机设备地址,如果发送的从机地址正确,就接着发送操作从机地址的command这个地址,因为这里报存着温度数据。然后重新发起一个起始信号,开始读数据,把温度数据的低位DataL高位DataH 和PEC数据读出来,再进行PEC数据校验,判断读到的PEC数据和校验得到的PEC数据是否相等,相等的话就跳出循环,通过将高位数据左移8位再与低8位进行按位或就得到了最终的数据data。6.得到最终温度值float SMBus_ReadTemp(void) float temp; temp = SMBus_ReadMemory(0x00, 0x07)*0.02-273.15; return temp; }  通过数据手册我们知道将最终读取的数据*0.02-273.15就会得到最终的温度实际值,通过串口打印或者oled显示就可以得到传感器读取的温度了。六、CRC­8校验原理1.模2除法  模2除法与算术除法类似,但每一位除的结果不影响其它位,即不向上一位借位,所以实际上就是异或。在循环冗余校验码(CRC)的计算中有应用到模2除法。CRC校验中有两个关键点,一是预先确定一个发送端和接收端都用来作为除数的二进制比特串(或多项式),可以随机选择,也可以使用国际标准,但是最高位和最低位必须为1;二是把原始帧与上面计算出的除数进行模2除法运算,计算出CRC码。  2.具体步骤选择合适的多项式,确定除数。看选定多项式的二进制位数,然后将要发送的数据上面加上这个位数­1位的0,然后用得到的数据以模2除法的方式除上面确定的除数,得到的余数就是该数的CRC校验码。注意,余数的位数一定只比除数位数少一位,也就是CRC校验码位数比除数位数少一位,如果前面位是0也不能省略。3.实例1现假设我们使用的多项式为:G(X) =X^ 8 + X^ 2+X^ 1+1,要求出0x1A的CRC­8校验码。下面是具体的计算过程:1、 将多项式转化为二进制序列,由G(X) = X^ 8 + X^ 2+X^ 1+1可知二进制一种有9位,第8位、第2位、第1位和第0位分别为1,则序列为100000111。2、原来要计算的数据为1 1010,多项式的最高次为8,则在数据的后面加上8位0,数据变为110100000 0000,然后使用模2除法除以除数100000111,最终得到的除不尽的余数,变为我们要求的CRC­8结果。3、最后除不尽的余数为0x46,所以0x1A按多项式G(X) = X8+X2+X+1计算得到的CRC­8码为0x46。4.示例2也就是求0xb4 0x07 0xb5 0xd2 0x3a 的CRC­8校验码是否是0x30将多项式转化为二进制序列,由G(X) = X^8+X^2+X^1+1可知二进制一种有9位,第8位、第2位、第1位和第0位分别为1,则序列为100000111。原来要计算的数据为b4 07 b5 d2 3a转换为2进制就是1011010000000111101101011101001000111010,多项式的最高次为8,则在数据的后面加上8位0,数据变为101101000000011110110101110100100011101000000000,然后使用模2除法除以除数100000111,最终得到的除不尽的余数,变为我们要求的CRC­8结果。5.读取温度函数u16 SMBus_ReadMemory(u8 slaveAddress, u8 command) u16 data; u8 Pec; u8 DataL=0; u8 DataH=0; u8 arr[6]; u8 PecReg; u8 ErrorCounter; ErrorCounter=0x00;// Initialising of ErrorCounter slaveAddress <<= 1; //2-7位表示从机地址 从机地址左移一位,把读写位空出来 repeat: SMBus_StopBit(); --ErrorCounter; if(!ErrorCounter) //ErrorCounter=0? break; //如果为0就跳出do-while{}循环 SMBus_StartBit(); if(SMBus_SendByte(slaveAddress))//发送从机地址最低位Wr=0表示接下来写命令 goto repeat; if(SMBus_SendByte(command))//发送命令 goto repeat; SMBus_StartBit(); if(SMBus_SendByte(slaveAddress+1)) //发送从机地址+1最低位Rd=1表示接下来读数据 goto repeat; DataL = SMBus_ReceiveByte(ACK); //读低位数据保存到DataL DataH = SMBus_ReceiveByte(ACK); //读高位数据保存到DataH Pec = SMBus_ReceiveByte(NACK); //读校验数据保存到Pec SMBus_StopBit(); arr[5] = slaveAddress; arr[4] = command; arr[3] = slaveAddress+1; arr[2] = DataL; arr[1] = DataH; arr[0] = 0; PecReg=PEC_Calculation(arr);//Calculate CRC 数据校验 while(PecReg != Pec); data = (DataH<<8) | DataL; return data; }  这个函数的输入参数就IIC设备的从机地址和command寄存器地址。首先从机地址左移一位,把读写位空出来,因为不知道最开始是啥状态,我们就需要发送一个停止信号,因为ErrorCounter=0x00;那么进行"--"操作之后就不等于0,就进行下面的操作。接下来就发送起始信号,发送从机设备地址,如果发送的从机地址正确,就接着发送操作从机地址的command这个地址,因为这里报存着温度数据。然后重新发起一个起始信号,开始读数据,把温度数据的低位DataL高位DataH 和PEC数据读出来,再进行PEC数据校验,判断读到的PEC数据和校验得到的PEC数据是否相等,相等的话就跳出循环,通过将高位数据左移8位再与低8位进行按位或就得到了最终的数据data。6.得到最终温度值float SMBus_ReadTemp(void) float temp; temp = SMBus_ReadMemory(0x00, 0x07)*0.02-273.15; return temp; }  通过数据手册我们知道将【最终读取的数据*0.02-273.15】就会得到最终的温度实际值,通过串口打印或者oled显示就可以得到传感器读取的温度了。后台回复:【红外测温】,即可免费获取红外测温程序源码。好书不厌百回读,熟读自知其中意。将学习成为习惯,用知识改变命运,用博客见证成长,用行动证明努力。如果我的博客对你有帮助、如果你喜欢我的博客内容,请 “==点赞” “评论” “收藏==” 一键三连哦!听说 点赞**的人运气不会太差,每一天都会元气满满呦!^_^❤️❤️❤️码字不易,大家的支持就是我坚持下去的动力。点赞后不要忘了 关注 我哦!**更多精彩内容请前往 果果小师弟的微信公众号如果以上内容有任何错误或者不准确的地方,欢迎在下面 留个言。或者你有更好的想法,欢迎一起交流学习~~~关注微信公众号:[果果小师弟],获取更多精彩内容!智果芯—服务于百万大学生和电子工程师

STM32第九章-IIC通讯应用

@[TOC]  说到IIC(通常也叫I2C,其实都是一样的)通讯,是一种最简单的通讯协议。在学习STM32时第一个接触的就是串口USART通讯协议,接下来就是IIC通讯协议了还有的就是SPI协议,SPI我们下一章再说,这一章就说说IIC吧。很多模块都用到过IIC通讯,最常见的就是4针的0.96寸OLED显示屏,当然啦在学习STM32是我们一般最先接触到就是通过IIC来与EEPROM进行通讯,但是本章我们只讲协议本身。一、 IIC 简介  IIC(Inter-Integrated Circuit)总线是一种由 PHILIPS 公司开发的两线式串行总线,用于连接微控制器及其外围设备。它是由数据线 SDA 和时钟 SCL 构成的串行总线,可发送和接收数据。在 CPU 与被控 IC 之间、IC 与 IC 之间进行双向传送,高速 IIC 总线一般可达 400kbps 以上。  I2C 总线在传送数据过程中共有三种类型信号, 它们分别是:开始信号、结束信号和应答信号。  开始信号:SCL 为高电平时,SDA 由高电平向低电平跳变,开始传送数据。  结束信号:SCL 为高电平时,SDA 由低电平向高电平跳变,结束传送数据。  应答信号:接收数据的 IC 在接收到 8bit 数据后,向发送数据的 IC 发出特定的低电平脉冲,表示已收到数据。CPU 向受控单元发出一个信号后,等待受控单元发出一个应答信号,CPU 接收到应答信号后,根据实际情况作出是否继续传递信号的判断。若未收到应答信号,由判断为受控单元出现故障。这些信号中,起始信号是必需的,结束信号和应答信号都可以不要。  IIC使用 SDA信号线来传输数据,使用 SCL信号线进行数据同步。SDA数据线在 SCL的每个时钟周期传输一位数据。传输时,SCL为高电平的时候 SDA 表示的数据有效,即此时的 SDA 为高电平时表示数据“1”,为低电平时表示数据“0”。当 SCL为低电平时,SDA的数据无效,一般在这个时候SDA进行电平切换,为下一次表示数据做好准备。每次数据传输都以字节为单位,每次传输的字节数不受限制。  如果我们直接控制STM32的两个GPIO 引脚,分别用作 SCL和SDA,按照上述信号的时序要求,直接像控制 LED 灯那样控制引脚的输出(若是接收数据时则读取 SDA电平),就可以实现 IIC通讯。同样假如我们按照 USART的要求去控制引脚,也能实现 USART通讯。所以只要遵守协议,就是标准的通讯,不管您如何实现它,不管是ST生产的控制器还是ATMEL生产的存储器, 都能按通讯标准交互。  由于直接控制 GPIO 引脚电平产生通讯时序时,需要由 CPU 控制每个时刻的引脚状态,所以称之为“软件模拟协议”方式。相对地,还有“硬件协议”方式,STM32 的 IIC片上外设专门负责实现IIC通讯协议,只要配置好该外设,它就会自动根据协议要求产生通讯信号,收发数据并缓存起来,CPU只要检测该外设的状态和访问数据寄存器,就能完成数据收发。这种由硬件外设处理IIC协议的方式减轻了 CPU 的工作,且使软件设计更加简单。二、通信特征:串行、同步、非差分、低速率I2C属于串行通信,所有的数据以位为单位在SDA线上串行传输。同步通信就是通信双方工作在同一个时钟下,一般是通信的A方通过一根CLK信号线传输A自己的时钟给B,B工作在A传输的时钟下。所以同步通信的显著特征就是:通信线中有CLK。非差分。因为I2C通信速率不高,而且通信双方距离很近,所以使用电平信号通信。低速率。I2C一般是用在同一个板子上的2个IC之间的通信,而且用来传输的数据量不大,所以本身通信速率很低(一般几百KHz,不同的I2C芯片的通信速率可能不同,具体在编程的时候要看自己所使用的设备允许的I2C通信最高速率,不能超过这个速率)突出特征 1:主设备+从设备(必须明确)I2C通信的时候,通信双方地位是不对等的,而是分主设备和从设备。通信由主设备发起,由主设备主导,从设备只是按照I2C协议被动的接受主设备的通信,并及时响应。谁是主设备、谁是从设备是由通信双方来定的(I2C协议并无规定),一般来说一个芯片可以只能做主设备、也可以只能做从设备、也可以既能当主设备又能当从设备(软件配置)。有很多人认为在通信时单片机是主设备,器件是从设备,这是不严谨的。STM32单片机也可以当从设备,只是你没见到过罢了。突出特征 2:可以多个设备挂在一条总线上(从设备地址)I2C 通信可以一对一(1个主设备对1个从设备),也可以一对多(1个主设备对多个从设备)。主设备来负责调度总线,决定某一时间和哪个从设备通信。注意:同一时间内,I2C 的总线上只能传输一对设备的通信信息,所以同一时间只能有一个从设备和主设备通信,其他从设备处于“冬眠”状态,不能出来捣乱,否则通信就乱套了。每一个 I2C 从设备在通信中都有一个 I2C 从设备地址,这个设备地址是从设备本身固有的属性,然后通信时主设备需要知道自己将要通信的那个从设备的地址,然后在通信中通过地址来甄别是不是自己要找的那个从设备。(这个地址是一个电路板上唯一的,不是全球唯一的)I2C 的总线空闲状态、起始位、结束位I2C 总线上有 1 个主设备,n(n>=1)个从设备。I2C 总线上有 2 种状态;空闲态(所有从设备都未和主设备通信,此时总线空闲)和忙态(其中一个从设备在和主设备通信,此时总线被这一对占用,其他从设备必须歇着)。整个通信分为一个周期一个周期的,两个相邻的通信周期是空闲态。每一个通信周期由一个起始位开始,一个结束位结束,中间是本周期的通信数据。起始位并不是一个时间点,起始位是一个时间段,在这段时间内总线状态变化情况是:SCL 线维持高电平,同时 SDA 线发生一个从高到低的下降沿。与起始位相似,结束位也是一个时间段。在这段时间内总线状态变化情况是:SCL 线维持高电平,同时 SDA 线发生一个从低到高的上升沿。**如何牢记IIC通信的起始信号和结束信号的时序?  我们把IIC通信看做一条游荡在水中小船,把船面看成SDA数据线,水面波澜起伏看成IIC通信的时钟SCK,没有水船就不能走,同理没有时钟线就没有通信。因为SCL 维持高电平,SDA 线发生一个从高到低的下降沿起始信号就开始了,所以我们可以把船头当做起始信号(想象一下,在湖面上一条弯弯的小船在顺水而行)。同时SCL 维持高电平,SDA 线发生一个从低到高的上升沿就是停止信号,故我们可以把船尾看做停止信号(小船的船尾是不是与水面夹角为45°)。因此如果你记不住IIC通信的时序,请想象一下,你坐在一条小船上,顺水而下坐在船上拿着电脑写着IIC驱动程序就可以了**。I2C 数据传输格式(数据位&ACK)每一个通信周期的发起和结束都是由主设备来做的,从设备只有被动的响应主设备,没法自己自发的去做任何事情。主设备在每个通信周期会先发8位的从设备地址(其实8位中只有7位是从设备地址,还有1位表示主设备下面要写入还是读出)到总线(主设备是以广播的形式发送的,只要是总线上的所有从设备其实都能收到这个信息)。然后总线上的每个从设备都能收到这个地址,并且收到地址后和自己的设备地址比较看是否相等。如果相等说明主设备本次通信就是给我说话,如果不想等说明这次通信与我无关,不用听了不管了。发送方发送一段数据后,接收方需要回应一个 ACK。这个响应本身只有1个 bit 位,不能携带有效信息,只能表示 2 个意思(要么表示收到数据,即有效响应;要么表示未收到数据,无效响应)。应答信号ACK只能是必须是接收方发送,因为只有发送方发送数据后接收方才能应答。在某一个通信时刻,主设备和从设备只能有一个在发(占用总线,也就是向总线写),另在每个时钟的上升沿,把要发送的数据准备好,发送的才有效应答信号ACK只能是必须是接收方发送,因为只有发送方发送数据后接收方才能应答一个在收(从总线读)。如果在某个时间主设备和从设备都试图向总线写那就完蛋了,通信就乱套了。数据在总线上的传输协议I2C通信时的基本数据单位也是以字节为单位的,每次传输的有效数据都是1个字节(8位)。起始位及其后的8个CLK中都是主设备在发送(主设备掌控总线),此时从设备只能读取总线,通过读总线来得知主设备发给从设备的信息;然后到了第9周期,按照协议规定从设备需要发送ACK给主设备,所以此时主设备必须释放总线(主设备把总线置为高电平然后不要动,其实就类似于总线空闲状态),同时从设备试图拉低总线后发出ACK。如果从设备拉低总线失败,或者从设备根本就没有拉低总线,则主设备看到的现象就是总线在第9周期仍然一直保持高,这对主设备来说,意味着我没收到ACK,主设备就认为刚才给从设备发送的8字节不对(接收失败)二、 软件模拟协议1.IIC初始化函数功能:配置IIC的时钟线和数据线void IIC_Init(void) GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOC, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12|GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); IIC_SCL=1; IIC_SDA=1; }  因为是软件模拟IIC那么我们选择IIC通讯的引脚就相对来说说比较随意,具体使用的引脚可查阅《STM32F1xx 规格书》,以它为准。这里我们就选择PC11、PC12作为IIC的数据和时钟引脚。设置为推挽输出即可。2.起始信号功能: CPU发起I2C总线起始信号void IIC_Start(void) IIC_SDA=1; IIC_SCL=1; delay_us(4); IIC_SDA=0;//START:当 CLK 为高电平时,DATA 从高到低改变 delay_us(4); IIC_SCL=0;//钳住I2C总线,准备发送或接收数据 delay_us(4); }  起始信号产生后,所有从机设备就开始等待STM32紧接下来的从机地址信号。在IIC总线上,每个设备的地址都是唯一的,当主机广播的地址与某个设备地址相同时,这个设备就被选中了,没被选中的设备将会忽略之后的数据信号。根据IIC协议,这个从机地址可以是 7位或10位。在地址位之后,是传输方向的选择位,该位为 0时,表示后面的数据传输方向是由主机传输至从机,即主机向从机写数据。该位为 1时,则相反,即主机由从机读数据。3.等待应答信号功能:CPU 产生一个时钟,并读取器件的 ACK 应答信号//返回值:1,接收应答失败 0,接收应答成功 u8 IIC_Wait_Ack(void) u8 re; IIC_SDA=1;delay_us(1);//CPU释放SDA总线 IIC_SCL=1;delay_us(1);//CPU驱动SCL=1,此时器件会返回ACK应答 if(READ_SDA){//CPU读取SDA口线状态 re=1; }else{ re=0; IIC_SCL=0;//时钟输出0 return re; }   该函数用于 STM32 作为发送方时,等待及处理接收方传来的响应或非响应信号, 即一般调用前面的 IIC_SendByte 函数后,再调用本函数检测响应。  STM32控制 SDA 信号线输出高阻态,释放它对 SDA的控制权,由接收方控制;控制 SCL 信号线切换高低电平,产生一个时钟信号,根据IIC协议,此时接收方若把 SDA 设置为低电平,就表示返回一个“应答”信号,若 SDA 保持为高电平,则表示返回一个“非应答 ”信号;在 SCL 切换高低电平之间,有个延时确保给予了足够的时间让接收方返回应答信号,延时后使用宏SDA_READ 读取 SDA 线的电平,根据电平值赋予 re 变量的值; 函数的最后返回 re的值,接收到响应时返回 0,未接收到响应时返回 1。当 STM32 作为数据接收端,调用 IIC_ReadByte 函数后,需要给发送端返回应答或非应答信号,此时可使用 IIC_Ack及 IIC_Nack 函数处理,该处理与 IIC_Wait_Ack函数相反,此时 SDA线也由 STM32控制。4.应答信号功能: CPU 产生一个 ACK 信号//CPU产生一个ACK信号 void IIC_Ack(void) IIC_SDA=0;//CPU驱动SDA=0 delay_us(2); IIC_SCL=1;//CPU产生一个时钟 delay_us(2); IIC_SCL=0; delay_us(2); IIC_SDA=1;//CPU释放SDA总线 //CPU产生1个NACK信号 void IIC_Nack (void) IIC_SDA=1();//CPU驱动SDA=1 delay_us(2); IIC_SDA=1;//CPU产生1个时钟 delay_us(2); IIC_SCL=0; delay_us(2); }  I2C 的数据和地址传输都带响应。响应包括“应答(ACK)”和“非应答(NACK)”两种信号。作为数据接收端时,当设备接收到 I2C 传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答(ACK)”信号,发送方会继续发送下一个数据;若接收端希望结束数据传输,则向对方发送“非应答(NACK)”信号,发送方接收到该信号后会产生一个停止信号,结束信号传输。  代码的具体流程就是:根据要返回“应答”还是“非应答”信号,先准备好 SDA 线的电平,IIC_Ack函数中把 SDA 线设置为低电平,表示“应答”信号,IIC_Nack 函数中把 SDA 线设置为高电平,表示“非应答”信号;控制 SCL 线进行高低电平切换,产生一个时钟信号,在 SCL 线的高低电平之间加入一个延时,确保有足够的时间让通讯的另一方接收到 SDA信号线的电平;在 IIC_Ack 函数的末尾,响应信号发送结束后,重新把 SDA 线设置为高电平以释放总线的控制权,方便后续的通讯。5.停止信号功能:CPU 发起 I2C 总线停止信号{ IIC_SDA=0;//STOP:当 CLK 为高电平时候, SDA出现一个上调表示IIC总线停止信号 IIC_SCL=1; delay_us(4); IIC_SDA=1;//发送I2C总线结束信号 }  停止信号直接看是时序图就可以搞定了,在SCL和SDA都为低电平的情况下,首先把时钟线SCL拉高,再把数据线SDA拉高,IIC就会结束传输了。  以上就是软件模拟IIC协议了,在平时的应用中我们实际上不需要掌握这些具体的代码,只要知道IIC协议的过程原理就行了,应为一般来说我们用的都是别人写好的代码,我们只需要会用就可以了,如果你的代码和我这些有出入也没有关系,只要能正常通讯即可,当然如果你的设计在过程中出现了一些问题,或者显示不正常,我们首先考虑的也不是底层协议的问题,而是你代码的其他问题。6.IIC发送字节功能: CPU向I2C总线设备发送8bit数据void IIC_SendByte(u8 Byte) u8 i; /* 先发送字节的高位bit7 */ for (i = 0; i < 8; i++) if (Byte & 0x80) IIC_SDA=1; IIC_SDA=0; i2c_Delay(); IIC_SCL=1; delay_us(2); IIC_SCL=0; if (i == 7) IIC_SDA=1;// 释放总线 Byte <<= 1; /* 左移一个bit */ delay_us(2); }  该函数以其输入参数作为要使用 I2C 协议输出的数据,该数据大小为一字节。函数的主体是一个 8 次的 for 循环,循环体执行一次将会对外发送一个数据位,循环结束时刚好发送完该字节数据。步骤分解如下:  首先程序对输入参数Byte 和 0x80“与”运算,判断其最高位的逻辑值,为 1 时控制 SDA输出高电平,为 0则控制 SDA输出低电平;接下来 延时,以此保证 SDA 线输出的电平已稳定,再进行后续操作;之后控制 SCL线产生高低电平跳变,也就是产生 I2C协议中 SCL线的通讯时钟; 在 SCL线高低电平之间有个延时,该延时期间 SCL线维持高电平,根据 I2C协议,此时数据有效,通讯的另一方会在此时读取 SDA 线的电平逻辑,高电平时接收到该位为数据 1,否则为 0;就这样一次循环体执行结束,Byte 左移一位以便下次循环发送下一位的数据;如次循环 8 次,把Byte 中的 8 位个数据位发送完毕,在最后一位发送完成后(此时循环计数器 i=7),控制 SDA 线输出 1(即高阻态),也就是说发送方释放 SDA总线,等待接收方的应答信号。7.IIC读取字节功能: CPU从IIC总线设备读取8bit数据u8_t IIC_ReadByte(void) u8 i; u8 value; //读到第1个bit为数据的bit7 value = 0; for (i = 0; i < 8; i++) value <<= 1; IIC_SCL=1; delay_us(2); if (DA_READ) value++; IIC_SCL=0; delay_us(2); return value; }  IIC_ReadByte 函数也是以 for 循环为主体,循环体会被执行 8次,执行完毕后将会接收到一个字节的数据,循环体接收数据的流程如下:  首先使用一个变量 value 暂存要接收的数据,每次循环开始前先对 value 的值左移 1 位,以给 value 变量的 bit0 腾出空间,bit0 将用于缓存最新接收到的数据位,一位一位地接收并移位,最后拼出完整的 8位数据;然后控制 SCL线进行高低电平切换,输出 I2C 协议通讯用的时钟; 在 SCL 线高低电平切换之间,有个延时,该延时确保给予了足够的时间让数据发送方进行处理,即发送方在 SCL 时钟驱动下通过 SDA 信号线发出电平逻辑信号,而这个延时之后,作为数据接收端的 STM32 使用宏 SDA_READ读取 SDA信号线的电平,若信号线为 1,则 value++,即把它的 bit0置 1,否则不操作(这样该位将保持为 0),这样就读取到了一位的数据;接下来SCL线切换成低电平后,加入延时,以便接收端根据需要切换 SDA 线输出数据;直到循环结束后,value 变量中包含有 1 个字节数据,使用 return 把它作为函数返回值返回;三、 硬件协议  相对来说,硬件IIC直接使用外设来控制引脚,可以减轻 CPU 的负担。不过使用硬件IIC 时必须使用某些固定的引脚作为 SCL 和 SDA,软件模拟IIC则可以使用任意 GPIO 引脚,相对比较灵活。  STM32的IIC外设可用作通讯的主机或从机,支持 100Kbit/s 和 400Kbit/s 的速率,支持 7位、10位设备地址,支持 DMA数据传输,并具有数据校验功能。它的IIC外设还支持 SMBus2.0协,SMBus 协议与IIC类似,主要应用于笔记本电脑的电池管理中。  STM32 芯片有多个IIC外设,它们的IIC通讯信号引出到不同的 GPIO 引脚上,使用时必须配置到这些指定的引脚,GPIO引脚的复用功能,可查阅《STM32F1xx 规格书》,以它为准。IIC初始化函数void IIC_init(void) GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;// 开漏输出 GPIO_Init(GPIOA, &GPIO_InitStructure); IIC_SCL=1; IIC_SDA=1;//给一个停止信号, 复位I2C总线上的所有设备到待机模式 }  因为是硬件IIC直接使用外设来控制引脚,那么我们选择IIC通讯的引脚就比较固定,具体使用的引脚可查阅《STM32F1xx 规格书》,以它为准。可以看到PB6和PB7两个引脚可以作为IIC的通讯引脚,而且PB6为SCL时钟线,而PB7则为SDA数据线,并设置为开漏输出。  这里为啥设置为开漏输出的方式呢?  这是由于使用的是软件模拟IIC方式,而IIC协议的 GPIO 必须的开漏输出模式,开漏输出模式在输出高电平时实际输出高阻态,当IIC该总线上所有设备都输出高阻态时,由外部的上拉电阻上拉为高电平。另外当 STM32 的 GPIO 配置成开漏输出模式时,它仍然可以通过读取GPIO 的输入数据寄存器获取外部对引脚的输入电平,也就是说它同时具有浮空输入模式的功能,因此在后面控制 SDA线对外输出电平或读取 SDA线的电平信号时不需要切换 GPIO的模式。  另外在应交IIC协议之下,它的起始信号、等待应答信号、应答信号、停止信号都与软件模拟IIC协议之下的函数相同,在这里我就不重复说明了。总结:IIC通讯协议很简单,在实际项目中我们不需要掌握具体的IIC协议代码,只要会用即可,作为最常见且常用的协议,我们最好能够背下来或者有所了解。现在IIC通讯不陌生了吧!关注微信公众号:[果果小师弟],获取更多精彩内容!智果芯—服务于百万大学生和电子工程师

STM32第六章-定时器详解

@TOC定时器(Timer)最基本的功能就是定时了,比如定时发送 USART 数据、定时采集 AD数据等等。如果把定时器与 GPIO 结合起来使用的话可以实现非常丰富的功能,可以测量输入信号的脉冲宽度,可以生产输出波形。定时器生产 PWM 控制电机状态是工业控制普遍方法,这方面知识非常有必要深入了解。STM32F4xx系列控制器有 2 个高级控制定时器、10 个通用定时器和 2 个基本定时器。这里通用定时器的时钟频率是由APB1的分频系数决定,如果APB1的预分频系数是1,则通用定时器的时钟频率等于APB1的时钟频率,否则为APB1时钟的2倍。时钟源定时器要实现计数必须有个时钟源,基本定时器时钟只能来自内部时钟,高级控制定时器和通用定时器还可以选择外部时钟源或者直接来自其他定时器等待模式。我们可以通过 RCC 专用时钟配置寄存器(RCC_DCKCFGR)的 TIMPRE位设置所有定时器的时钟频率,我们一般设置该位为默认值 0,使得表中可选的最大定时器时钟为 90MHz,即基本定时器的内部时钟(CK_INT)频率为 90MHz。基本定时器只能使用内部时钟,当 TIM6 和 TIM7 控制寄存器 1(TIMx_CR1)的 CEN 位置 1时,启动基本定时器,并且预分频器的时钟来源就是 CK_INT。对于高级控制定时器和通用定时器的时钟源可以来找控制器外部时钟、其他定时器等等模式,较为复杂。使用SystenInit函数初始化的时候,各时钟频率如下:SYSCLK = 72MAHB时钟 = 72MAPB1时钟=36M所以APB1的分频系数=AHB/APB1=2由此可得CK_INT的时钟频率为2*36M = 72M.计数器的最终的频率还需要经过PSC预分频计算才能得到计数器基本定时器计数过程主要涉及到三个寄存器内容,分别是计数器寄存器(TIMx_CNT)、预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR),这三个寄存器都是 16 位有效数字,即可设置值为 0至 65535。定时器周期计算定时事件生成时间主要由 TIMx_PSC 和 TIMx_ARR两个寄存器值决定,这个也就是定时器的周期。比如我们需要一个 1s周期的定时器,具体这两个寄存器值该如何设置?**假设,我们先设置 TIMx_ARR寄存器值为 9999,即当 TIMx_CNT从 0开始计算,刚好等于 9999时生成事件,总共计数 10000次,那么如果此时时钟源周期为 100us即可得到刚好 1s的定时周期。接下来问题就是设置 TIMx_PSC寄存器值使得 CK_CNT 输出为 100us 周期(10000Hz)的时钟。预分频器的输入时钟 CK_PSC为 90MHz,所以设置预分频器值为(9000-1)即可满足。**定时器初始化结构体详解typedef struct uint16_t TIM_Prescaler; // 预分频器 uint16_t TIM_CounterMode; // 计数模式 uint32_t TIM_Period; // 定时器周期 uint16_t TIM_ClockDivision; // 时钟分频 uint8_t TIM_RepetitionCounter; // 重复计算器 } TIM_TimeBaseInitTypeDef;(1) TIM_Prescaler:定时器预分频器设置,时钟源经该预分频器才是定时器时钟,它设定TIMx_PSC 寄存器的值。可设置范围为 0 至 65535,实现 1至 65536 分频。为啥要搞一个预分频器,那是因为系统时钟频率太快了,90MHZ啊,这一般人定时器可顶不住这么快的速度,所以分频一下,让他的给定时器的时钟频率少一点,仅此而已。(2) TIM_CounterMode:定时器计数方式,可是在为向上计数、向下计数以及三种中心对齐模式。基本定时器只能是向上计数,即 TIMx_CNT只能从 0开始递增,并且无需初始化。(3) TIM_Period:定时器周期,实际就是设定自动重载寄存器的值,在事件生成时更新到影子寄存器。可设置范围为 0至 65535。自动重载寄存器的值:举个例子,你要往桶里面放水,水满了之后把他倒掉。那水满需要多少水呢?就给他设定一个值,滴水滴100000滴才满,拿去倒掉。倒掉之后,在重新设置滴10000滴,满了再倒掉......(4) TIM_ClockDivision:时钟分频,设置定时器时钟 CK_INT 频率与数字滤波器采样时钟频率分频比,基本定时器没有此功能,不用设置。(5) TIM_RepetitionCounter:重复计数器,属于高级控制寄存器专用寄存器位,利用它可以非常容易控制输出 PWM 的个数。这里不用设置.程序设置设置通用定时器,并产生相应中断,主要分为以下几个步骤(以TIM3为例)TIM3时钟使能设置TIM3_ARR和TIM3_PSC的值设置TIM3_DIER允许更新中断允许TIM3工作TIM3中断分组设置编写中断服务函数void TIM3_Int_Init() TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //时钟使能 TIM_TimeBaseStructure.TIM_Prescaler =7199; //设置用来作为TIMx时钟频率除数的预分频值 10Khz的计数频率 TIM_TimeBaseStructure.TIM_Period = 4999; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值 计数到5000为500ms TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式 TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位 TIM_ITConfig(TIM3, TIM_IT_Update,ENABLE); NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; //TIM3中断 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //先占优先级0级 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //从优先级3级 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能 NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器 TIM_Cmd(TIM3, ENABLE); //使能TIMx外设 }使用定时器之前都必须开启定时器时钟,基本定时器属于 APB1总线外设。APB1总线外设时钟=72M。 我们把定时器设置自动重装载寄存器 arr 的值为4999,设置时钟预分频器寄存器psc的值为7199,则驱动计数器的时钟:CK_CNT = APB1Periph/ (7199+1)=72M/7200=10K,则计数器计数一次的时间等于:1/CK_CNT=0.0001s=0.1ms=100us,当计数器从0计数到4999时,产生一次中断,则中断一次的时间为:100usX5000=0.0001sX5000=0.5s=500ms也就是半秒钟。void TIM3_IRQHandler(void) //TIM3中断 if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) //检查指定的TIM中断发生与否:TIM 中断源 TIM_ClearITPendingBit(TIM3, TIM_IT_Update ); //清除TIMx的中断待处理位:TIM 中断源 LED1=!LED1; }这个中断服务函数开始用if语句、TIM_GetITStatus()函数判断是否TIM3发生了中断,如果发生了中断就清除TIM3的中断标志位。让LED1灯反转。 int main(void) delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);// 设置中断优先级分组2 LED_Init(); //初始化与LED连接的硬件接口 TIM3_Int_Init(); //10Khz的计数频率,计数到5000为500ms while(1) LED0=!LED0; delay_ms(200); }主函数首先延时函数初始化、设置中断优先级分组2、初始化与LED连接的硬件接口、定时器3初始化。至此LED0就会每个0.5秒翻转一下。同时我们为了比较在while函数中让LED1灯0.2秒翻转一下做对比。现在我们来用keil仿真一下,看看是不是LED0是0.2ms翻转一下,LED1是0.5ms翻转一下。1.配置keil仿真调试工具。2.打开调试, 进入调试界面后 ,打开logic analysis窗口,并设置PWM输出引脚3.点击全速运行,观察示波器可以清楚到看到LED0是0.5ms翻转一次。4.用同样的方法配置另一个LED1,发现是0.2ms翻转一下,和程序配置的一样。总结:我们使用定时器的时候主要是要清楚分频系数和重装载值。这两个决定了你要定时多长时间,另外如若你想使用stm32定时器,定时功能的话,需要定时多长时间做相应的操作,直接在中断服务函数里面写你的代码就行了,用中断也一样,要干啥事直接在你的响应的服务函数中做操作就行了。还有定时器和计数器的分别,定时器就是计数器啊。你先计数,计数到一定程度溢出不就是实现了定时吗?所以初学者不要纠结。上一篇:STM32第五章-串口通讯详解下一篇:STM32第七章-脉冲宽度调制关注微信公众号:[果果小师弟],获取更多精彩内容!智果芯—服务于百万大学生和电子工程师

STM32第二章-启动过程详解

STM32 的启动过程,启动过程是指从 CPU 上电复位执行第 1 条指令开始(汇编文件)到进入 C 程序 main()函数入口之间的部分。启动过程相对来说还是比较重要的,虽然难但必须了解掌握。1.不同的系列芯片的的启动代码不同。2.启动过程主要完成的工作:3.打开你的工程,鼠标双击工程文件。就会出来对应的.out文件查看中断向量列表在内部flash的存储。4.复位序列硬件复位之后,就是按下复位开关后。CPU 内的时序逻辑电路首先完成如下两个工作(程序代码下载到内部flash为例,flash首地址 0x0800 0000)将 0x08000000 位置存放的堆栈栈顶地址存放到 SP 中(MSP)。将 0x08000004 位置存放的向量地址装入 PC 程序计数器。CPU 从 PC 寄存器指向的物理地址取出第 1 条指令开始执行程序,也就是开始执行复位中断服务程序 Reset_Handler。复位中断服务程序会调用SystemInit()函数来配置系统时钟、配置FMC总线上的外部SRAM/SDRAM,然后跳转到 C 库中__main 函数。由 C 库中的__main 函数完成用户程序的初始化工作(比如:变量赋初值等),最后由__main 函数调用用户写的 main()函数开始执行 C 程序。学习启动文件之前先来了解一下汇编语言中的一些指令操作。具体代码分析第 1 部分代码分析下面的代码实现开辟栈(stack)空间,用于局部变量、函数调用、函数的参数等。栈的作用是用于局部变量,函数调用,函数形参等的开销,栈的大小不能超过内部SRAM 的大小。如果编写的程序比较大,定义的局部变量很多,那么就需要修改栈的大小。如果某一天,你写的程序出现了莫名奇怪的错误,并进入了硬 fault的时候,这时你就要考虑下是不是栈不够大,溢出了。第 7 行:EQU 是表示宏定义的伪指令,类似于 C 语言中的#define。伪指令的意思是指这个“指令”并不会生成二进制程序代码,也不会引起变量空间分配。0x00008000 表示栈大小,注意这里是以字节为单位。0x00008000 =32768字节=32KB第 8 行:开辟一段数据空间可读可写,段名 STACK,按照 8 字节对齐。ARER 伪指令表示下面将开始定义一个代码段或者数据段。此处是定义数据段。ARER 后面的关键字表示这个段的属性。STACK :表示这个段的名字,可以任意命名。NOINIT:表示此数据段不需要填入初始数据。READWRITE:表示此段可读可写。ALIGN=3 :表示首地址按照 2 的 3 次方对齐,也就是按照 8 字节对齐(地址对 8 求余数等于0)。第 9 行:SPACE 这行指令告诉汇编器给 STACK 段分配 0x00000800 字节的连续内存空间。第 10 行: __initial_sp 紧接着 SPACE 语句放置,表示了栈顶地址。__initial_sp 只是一个标号,标号主要用于表示一片内存空间的某个位置,等价于 C 语言中的“地址”概念。地址仅仅表示存储空间的一个位置,从 C 语言的角度来看,变量的地址,数组的地址或是函数的入口地址在本质上并无区别。第 2 部分代码分析下面的代码实现开辟堆(heap)空间,主要用于动态内存分配,像 malloc,calloc, realloc 等函数分配的变量空间是在堆上。这几行语句和上面第 1 部分代码类似。分配一片连续的内存空间给名字叫 HEAP 的段,也就是分配堆空间。堆的大小为 0x00000400,也就是1024字节=1KB。__heap_base 表示堆的开始地址。__heap_limit 表示堆的结束地址。第 3部分代码分析第 24 行:PRESERVE8 指定当前文件保持堆栈8字节对齐。第 25 行:THUMB 表示后面的指令是 THUMB 指令集 ,CM4 采用的是 THUMB - 2 指令集。第 29 行:AREA 定义一块代码段,只读,段名字是 RESET。READONLY 表示只读,缺省就表示代码段了。第 30-32 行:3 行 EXPORT 语句将 3 个标号申明为可被外部引用, 主要提供给链接器用于连接库文件或其他文件。当内核响应了一个发生的异常后,对应的异常服务例程(ESR)就会执行。为了决定 ESR的入口地址, 内核使用了―向量表查表机制‖。这里使用一张向量表。向量表其实是一个WORD( 32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。向量表在地址空间中的位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为 0。因此,在地址 0 (即 FLASH 地址 0)处必须包含一张向量表,用于初始时的异常分配。要注意的是这里有个另类: 0 号类型并不是什么入口地址,而是给出了复位后 MSP 的初值。第 4部分代码分析上面的这段代码是建立中断向量表,中断向量表定位在代码段的最前面。具体的物理地址由链接器的配置参数(IROM1 的地址)决定。如果程序在 Flash 运行,则中断向量表的起始地址是 0x08000000。以 MDK 为例,就是如下配置选项:DCD 表示分配 1 个 4 字节的空间。每行 DCD 都会生成一个 4 字节的二进制代码。中断向量表存放的实际上是中断服务程序的入口地址。当异常(也即是中断事件)发生时,CPU 的中断系统会将相应的入口地址赋值给 PC 程序计数器,之后就开始执行中断服务程序。第 5 部分代码分析第 53 行:AREA 定义一块代码段,只读,段名字是 .text 。READONLY 表示只读。第 56 行:利用 PROC、ENDP 这一对伪指令把程序段分为若干个过程,使程序的结构加清晰。第 57 行:WEAK 声明其他的同名标号优先于该标号被引用,就是说如果外面声明了的话会调用外面的。 这个声明很重要,它让我们可以在 C 文件中任意地方放置中断服务程序,只要保证 C 函数的名字和向量表中的名字一致即可。第 58 行:IMPORT:伪指令用于通知编译器要使用的标号在其他的源文件中定义。但要在当前源文件中引用,而且无论当前源文件是否引用该标号,该标号均会被加入到当前源文件的符号表中。第 61 行:SystemInit()是一个标准的库函数,在 system_stm32f4xx.c这个库文件总定义。主要作用是配置系统时钟,这里调用这个函数之后,F429的系统时钟配被配置为 180M。第 63 行:__main 标号表示 C/C++标准实时库函数里的一个初始化子程序__main 的入口地址。该程序的一个主要作用是初始化堆栈,并初始化映像文件,最后跳转到 C 程序中的 main 函数。这就解释了为何所有的 C 程序必须有一个 main 函数作为程序的起点。因为这是由 C/C++标准实时库所规,并且不能更改。如果我们在这里不调用__main,那么程序最终就不会调用我们 C文件里面的 main,如果是调皮的用户就可以修改主函数的名称,然后在这里面 IMPORT 你写的主函数名称即可。这个时候你在 C文件里面写的主函数名称就不是 main 了,而是 __main 了。LDR、BLX、BX 是 CM4内核的指令:第 6 部分代码分析第 71 行:死循环,用户可以在此实现自己的中断服务程序。不过很少在这里实现中断服务程序,一般多是在其它的 C 文件里面重新写一个同样名字的中断服务程序,因为这里是 WEEK 弱定义的。如果没有在其它文件中写中断服务器程序,且使能了此中断,进入到这里后,会让程序卡在这个地方。第 81 行:缺省中断服务程序(开始)第 92 行:死循环,如果用户使能中断服务程序,而没有在 C 文件里面写中断服务程序的话,都会进入到这里。比如在程序里面使能了串口 1 中断,而没有写中断服务程序 ART1_IRQHandle,那么串口中断来了,会进入到这个死循环。第 94 行:缺省中断服务程序(结束)。第 7 部分代码分析启动代码的最后一部分:第 101 行:简单的汇编语言实现 IF…….ELSE…………语句。如果定义了 MICROLIB,那么程序是不会执行 ELSE分支的代码。__MICROLIB 可能大家并不陌生,就在 MDK 的 Target Option 里面设置。局外话,这一步的配置工作很重要,很多人串口用不了 printf 函数,编译有问题,下载有问题,都是这个步骤的配置出了错。Target中选中微库“ Use MicroLib”,为的是在日后编写串口驱动的时候可以使用printf 函数。而且有些应用中如果用了 STM32 的浮点运算单元 FPU,一定要同时开微库,不然有时会出现各种奇怪的现象。FPU 的开关选项在微库配置选项下方的“Use Single Precision”中,默认是开的。上一篇:STM32第一章-寄存器你懂吗下一篇:STM32第三章-系统时钟配置关注微信公众号:[果果小师弟],获取更多精彩内容!智果芯—服务于百万大学生和电子工程师

STM32第一章-寄存器你懂吗?

一、什么是嵌入式?嵌入式系统是小型计算机的一个分支系统。平常用的PC,就属于功能比较专一的计算机,从核心的处理器来说,可以分成嵌入式微处理器和嵌入式微控制器,我们传统意义上的那种单片机,比如说像51、AVR还有按里面比较低配的一些,比如说像Cortex-M系列的这一类,我们都把它划分为微控制器,微处理器呢,就相对来说处理能力,运算能力要强一些,比如ARM9以上的系列和 Cortex-A以及以上系列。STM32属于一个微控制器,请大家牢牢记住微控制器这四个字。STM32自带了各种常用通信接口,比如USART、I2C、SPI等,可接非常多的传感器,可以控制很多的设备。现实生活中,我们接触到的很多电器产品都有STM32的身影,比如智能手环,微型四轴飞行器,平衡车、移动POST机,智能电饭锅,3D打印机等等。二、STM32长啥样以STM32F429IGT6为例。芯片正面是丝印,ARM表示该芯片使用的是ARM的内核,STM32F429IGT6是芯片型号。芯片四周是引脚,左下角的小圆点表示1脚,然后从1脚起按照逆时针的顺序排列(所有芯片的引脚顺序都是逆时针排列的)。开发板中把芯片的引脚引出来,连接到各种传感器上,然后在STM32上编程(实际就是通过程序控制这些引脚输出高电平或者低电平)来控制各种传感器工作,通过做实验的方式来学习STM32芯片的各个资源。三、芯片里面有什么我们看到的STM32芯片已经是已经封装好的成品,主要由内核和片上外设组成。若与电脑类比,内核与外设就如同电脑上的CPU与主板、内存、显卡、硬盘的关系。STM32F429采用的是Cortex-M4内核,内核即CPU,由ARM公司设计。ARM公司并不生产芯片,而是出售其芯片技术授权。芯片生产厂商(SOC)如ST、TI、Freescale,负责在内核之外设计部件并生产整个芯片,这些内核之外的部件被称为核外外设或片上外设。如GPIO、USART(串口)、I2C、SPI等都叫做片上外设。四、存储器映射请牢记住这一句话连接被控总线的是FLASH,RAM和片上外设,这些功能部件共同排列在一个4GB的地址空间内。我们在编程的时候,操作的也正是这些功能部件。存储器本身不具有地址信息,它的地址是由芯片厂商或用户分配,给存储器分配地址的过程就称为存储器映射。如果给存储器再分配一个地址就叫存储器重映射。这个图非常非常重要,初学者可能看不懂这个图。接写来我就详细的将讲解这一张图,让你真正的明白什么是内存,什么是寄存器,什么是寄存器映射。首先看这个图最左边的一竖排方格。我们前面说到连接被控总线的是FLASH,RAM和片上外设,这些功能部件共同排列在一个4GB的地址空间内。这里的4GB的地址空间就分布在这个图最左边的一竖排方格中。从0x0000 0000到0xFFFF FFFF。一看到这个东西大家可能又不知道这是个啥,为啥子地址要这样写。0x代表16进制,其实一看到F就知道这是十六进制了(0 1 2 3 4 5 6 7 8 9 A B C D E F16个数)。那0x0000 0000到0xFFFF FFFF是怎么算成是4GB大小的呢?我们首先把这些16进制化成2进制来看看,就是从0000 0000 0000 0000 0000 0000 0000 0000 到 1111 1111 1111 1111 1111 1111 1111 1111 要清楚16进制的1位数在2进制中就要表示为4位。例如16进制的F在二进制中2就表示为1111。从0000 0000 0000 0000 0000 0000 0000 0000 到 1111 1111 1111 1111 1111 1111 1111 1111 一共有2^(32)=4294967296个字节。4294967296byte/1024=4194304KB,4194304KB/1024=4096MB,4096MB/1024=4GB.额外的小知识1TByte=1024GByte,1GByte=1024MByte,1MByte=1024KByte,(1MB=1024*1024个字节)1KByte=1024Byte, (1KB=1024个字节)1Byte=8bit(一个字节由8个二进制位组成) int i;int类型占4个字节就是4*8=32个bit位(最大可表示的数为2^31-1=2147483647)double i;double类型占8个字节就是8*8=64个bit位(最大可表示的数为2^63-1)至此我们就清楚了4GB的大小空间,在内存中有多少的个地址。在这4GB中,分为8块。每一块的大小就是512MB字节。其中第三块,也就是Block2地址从0x4000 0000 到 0x5FFF FFFF分配给了我们的片上外设用如GPIO、USART(串口)、I2C、SPI等。在这8个Block里面,有3个块非常重要,也是我们最关心的三个块。Boock0用来设计成内部FLASH,Block1用来设计成内部RAM,Block2用来设计成片上的外设。4.1、存储器Block0内部区域功能划分Block0主要用于设计片内的FLASH,F429系列片内部FLASH最大是2MB,我们使用的STM32F429IGT6的FLASH是1MB。要在芯片内部集成更大的FLASH或者SRAM都意味着芯片成本的增加,往往片内集成的FLASH都不会太大,ST能在追求性价比的同时做到1MB以上,实乃良心之举。Block内部区域的功能划分具体见下图。4.2、储存器Block1内部区域功能划分Block1用于设计片内的SRAM。F429内部SRAM的大小为256KB,其中64KB的CCM RAM位于Block0,剩下的192KB位于Block1,分SRAM1112KB,SRAM216KB,SRAM364KB,Block内部区域的功能划分具体见下图。4.3、储存器Block2内部区域功能划分Block2用于设计片内的外设,根据外设的总线速度不同,Block被分成了APB和AHB两部分,其中APB又被分为APB1和APB2,AHB分为AHB1和AHB2,从小到大依次是APB1、APB2、AHB1、AHB1。具体见下图。还有一个AHB3包含了Block3/4/5/6,这四个Block用于扩展外部存储器,如SDRAM,NORFLASH和NANDFLASH等。在这里我希望大家一看到地址就要知道它是在内存的哪一个区域的,要会熟练地把地址和内存大小空间联系起来。如果你第一次不懂没关系,在后面遇到了一定要回过头来再看一遍,直到看懂为止。五、寄存器映射上面讲的是存储器映射,就是给存储器划分大小,分配地址,给存储器编号。下面讲的是寄存器映射,就是给寄存器划分大小,分配地址,给寄存器编号。在存储器 Block2 这块区域,设计的是片上外设,它们以4个字节为一个单元,共4*8=32bit,每一个单元对应不同的功能,当我们控制这些单元时就可以驱动外设工作。我们可以找到每个单元的起始地址,然后通过C语言指针的操作方式来访问这些单元,如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错,这时我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。比如,我们找到 GPIOH 端口的输出数据寄存器 ODR的地址是 0x40021C14(至于这个地址如何找到可以先跳过,后面我们会有详细的讲解),ODR 寄存器是 32bit,就是说ODR 寄存器在内存中占据4个空位即四个地址(我觉得这样说比较形象,比较好理解)低16bit(后面两个格子可以操作)有效,对应着 16个外部 IO,写 0/1 对应的的 IO 则输出低/高电平。现在我们通过 C语言指针的操作方式,让 GPIOH 的16个IO都输出高电平。// GPIOH 端口全部输出 高电平 *(unsigned int*)(0x4002 1C14) = 0xFFFF;有人会说咋不写0x0000 FFFF 这样看不是更直观吗?其实0x0000 FFFF和0xFFFF表示的意思一样,就比如1和01都表示1,直接写1不是更简单吗?对吧。但是你有这个想法很好,说明你在思考。0x40021C14在我们看来是 GPIOH端口 ODR的地址,但是在编译器看来,这只是一个普通的变量,是一个立即数,要想让编译器也认为是指针,我们得进行强制类型转换,把它转换成指针,即(unsigned int )0x40021C14,然后再对这个指针进行 操作。刚刚我们说了,通过绝对地址访问内存单元不好记忆且容易出错,我们可以通过寄存器的方式来操作.// GPIOH 端口全部输出 高电平 # define GPIOH_ODR *(unsigned int*)(GPIOH_BASE+0x14) GPIOH_ODR = 0xFF;5.1.STM32的外设地址映射上面讲的是存储器映射,就是给存储器划分大小,分配地址,给存储器编号。 寄存器映射,就是给寄存器划分大小,分配地址,给寄存器编号。下面讲STM32的外设地址映射,就是给外设地址划分大小,重新分配地址,给外设地址编号。片上外设区分为四条总线,根据外设速度的不同,不同总线挂载着不同的外设,APB挂载低速外设,AHB挂载高速外设。相应总线的最低地址我们称为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址。其中 APB1总线的地址最低,片上外设从这里开始,也叫外设基地址。5.1.1总线基地址5.1.2外设基地址总线上挂载着各种外设,这些外设也有自己的地址范围,特定外设的首个地址称为、“XX外设基地址”,也叫 XX外设的边界地址。GPIOA的基址相对于 AHB1总线的地址偏移为 0,我们应该就可以猜到,AHB1总线的第一个外设就是 GPIOA。5.1.3外设寄存器在 XX外设的地址范围内,分布着的就是该外设的寄存器。以 GPIO 外设为例,GPIO是通用输入输出端口的简称,简单来说就是 STM32可控制的引脚,基本功能是控制引脚输出高电平或者低电平。最简单的应用就是把 GPIO 的引脚连接到 LED 灯的阴极,LED 灯的阳极接电源,然后通过 STM32控制该引脚的电平,从而实现控制 LED 灯的亮灭。GPIO有很多个寄存器,每一个都有特定的功能。每个寄存器为 32bit,占四个字节,在该外设的基地址上按照顺序排列,寄存器的位置都以相对该外设基地址的偏移地址来描述。这里我们以 GPIOH 端口为例,来说明 GPIO 都有哪些寄存器这里我们以“GPIO 端口置位/复位寄存器”为例,教大家如何理解寄存器的说明,)名称寄存器说明中首先列出了该寄存器中的名称,“(GPIOx_BSRR)(x=A…I)”这段的意思是该寄存器名为“GPIOx_BSRR”其中的“x”可以为 A-I,也就是说这个寄存器说明适用于 GPIOA、GPIOB 至 GPIOI,这些 GPIO 端口都有这样的一个寄存器。偏移地址偏移地址是指本寄存器相对于这个外设的基地址的偏移。本寄存器的偏移地址是 0x18,从参考手册中我们可以查到 GPIOA外设的基地址为 0x4002 0000 ,就是AHB1这一块内存的首地址,GPIO是挂载在我们AHB1的总线上面的。我们就可以算出GPIOA的这个 GPIOA_BSRR 寄存器的地址为:0x4002 0000+0x18 ;同理,由于 GPIOB的外设基地址为 0x4002 0400,可算出 GPIOB_BSRR 寄存器的地址为:0x4002 0400+0x18 。其他 GPIO端口以此类推即可。寄存器位表紧接着的是本寄存器的位表,表中列出它的 0-31位的名称及权限。表上方的数字为位编号,中间为位名称,最下方为读写权限,其中 w 表示只写,r 表示只读,rw 表示可读写。本寄存器中的位权限都是 w,所以只能写,如果读本寄存器,是无法保证读取到它真正内容的。而有的寄存器位只读,一般是用于表示 STM32 外设的某种工作状态的,由 STM32硬件自动更改,程序通过读取那些寄存器位来判断外设的工作状态。位功能说明位功能是寄存器说明中最重要的部分,它详细介绍了寄存器每一个位的功能。例如本寄存器中有两种寄存器位,分别为 BRy及 BSy,其中的 y数值可以是 0-15,这里的 0-15表示端口的引脚号,如 BR0、BS0用于控制 GPIOx的第 0 个引脚,若 x表示 GPIOA,那就是控制 GPIOA的第 0引脚,而 BR1、BS1 就是控制 GPIOA第 1个引脚。其中 BRy引脚的说明是“0:不会对相应的 ODRx 位执行任何操作;1:对相应 ODRx位进行复位”。这里的“复位”是将该位设置为 0的意思,而“置位”表示将该位设置为1;说明中的 ODRx 是GPIO的另一个寄存器的寄存器位,我们只需要知道 ODRx位为 1 的时候,对应的引脚 x输出高电平,为 0 的时候对应的引脚输出低电平。所以,如果对 BR0 写入“1”的话,那么 GPIOx的第0 个引脚就会输出“低电平”,但是对 BR0写入“0”的话,却不会影响 ODR0位,所以引脚电平不会改变。要想该引脚输出“高电平”,就需要对“BS0”位写入“1”,寄存器位BSy与 BRy是相反的操作。这个功能说明建议多读几遍,反复的去读,直达彻底理解为止。5.2.C语言对寄存器的封装5.2.1封装总线和外设基地址在编程上为了方便理解和记忆,我们把总线基地址和外设基地址都以相应的宏定义起来,总线或者外设都以他们的名字作为宏名/* 外设基地址 */ #define PERIPH_BASE ((uint32_t)0x40000000) // 0x40000000是APB1的首地址,请看最前面的的那张图 #define APB1PERIPH_BASE PERIPH_BASE//使用宏定义 用APB1PERIPH_BASE代替PERIPH_BASE //下面依次内推即可得到GPIOA_BASE~GPIOK_BASE /* 总线基地址 */ #define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000) #define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000) #define AHB2PERIPH_BASE (PERIPH_BASE + 0x10000000) /* GPIO 外设基地址 */ #define GPIOA_BASE (AHB1PERIPH_BASE + 0x0000) #define GPIOB_BASE (AHB1PERIPH_BASE + 0x0400) #define GPIOC_BASE (AHB1PERIPH_BASE + 0x0800) #define GPIOD_BASE (AHB1PERIPH_BASE + 0x0C00) #define GPIOE_BASE (AHB1PERIPH_BASE + 0x1000) #define GPIOF_BASE (AHB1PERIPH_BASE + 0x1400) #define GPIOG_BASE (AHB1PERIPH_BASE + 0x1800) #define GPIOH_BASE (AHB1PERIPH_BASE + 0x1C00) #define GPIOI_BASE (AHB1PERIPH_BASE + 0x2000) #define GPIOJ_BASE (AHB1PERIPH_BASE + 0x2400) #define GPIOK_BASE (AHB1PERIPH_BASE + 0x2800) /* 寄存器基地址,以 GPIOA 为例 */ #define GPIOA_MODER (GPIOA_BASE+0x00) #define GPIOA_OTYPER (GPIOA_BASE+0x04) #define GPIOA_OSPEEDR (GPIOA_BASE+0x08) #define GPIOA_PUPDR (GPIOA_BASE+0x0C) #define GPIOA_IDR (GPIOA_BASE+0x10) #define GPIOA_ODR (GPIOA_BASE+0x14) #define GPIOA_BSRR (GPIOA_BASE+0x18) #define GPIOA_LCKR (GPIOA_BASE+0x1C) #define GPIOA_AFRL (GPIOA_BASE+0x20) #define GPIOA_AFRH (GPIOA_BASE+0x24)代码首先定义了 “片上外设”基地址 PERIPH_BASE(0x40000000),接着在 PERIPH_BASE 上加入各个总线的地址偏移,得到 APB1、APB2 等总线的地址 APB1PERIPH_BASE、APB2PERIPH_BASE,在其之上加入外设地址的偏移,得到 GPIOA到GPIOK的外设地址,最后在外设地址上加入各寄存器的地址偏移,得到特定寄存器的地址。一旦有了具体地址,就可以用指针操作读写了,具体见代码/* 控制 GPIOA 引脚 10 输出低电平(BSRR 寄存器的 BR10 置 1) */ *(unsigned int *)GPIOA_BSRR = (0x01<<(16+10)); /* 控制 GPIOA 引脚 10 输出高电平(BSRR 寄存器的 BS10 置 1) */ *(unsigned int *)GPIOA_BSRR = 0x01<<10; unsigned int temp; /* 控制 GPIOH 端口所有引脚的电平(读 IDR 寄存器) */ temp = *(unsigned int *)GPIOA_IDR;该代码使用 (unsigned int) 把 GPIOA_BSRR宏的数值强制转换成了地址,然后再用“”号做取指针操作,对该地址的赋值,从而实现了写寄存器的功能。同样,读寄存器也是用取指针操作,把寄存器中的数据取到变量里,从而获取 STM32外设的状态。5.2.2封装寄存器列表用上面的方法去定义地址,还是稍显繁琐,例如 GPIOA-GPIOH 都各有一组功能相同的寄存器,如 PIOA_MODER和GPIOB_MODER和GPIOC_MODER 等等,它们只是地址不一样,但却要为每个寄存器都定义它的地址。为了更方便地访问寄存器,我们引入 C语言中的结构体语法对寄存器进行封装,具体见代码typedef unsigned int uint32_t; /*无符号 32 位变量 占4个字节*/ typedef unsigned short int uint16_t; /*无符号 16 位变量 占2个字节*/ /* GPIO 寄存器列表 */ typedef struct uint32_t MODER; /*GPIO 模式寄存器 地址偏移: 0x00 */ uint32_t OTYPER; /*GPIO 输出类型寄存器 地址偏移: 0x04 */ uint32_t OSPEEDR; /*GPIO 输出速度寄存器 地址偏移: 0x08 */ uint32_t PUPDR; /*GPIO 上拉/下拉寄存器 地址偏移: 0x0C */ uint32_t IDR; /*GPIO 输入数据寄存器 地址偏移: 0x10 */ uint32_t ODR; /*GPIO 输出数据寄存器 地址偏移: 0x14 */ uint16_t BSRRL; /*GPIO 置位/复位寄存器低 16 位部分 地址偏移: 0x18 */ uint16_t BSRRH; /*GPIO 置位/复位寄存器高 16 位部分 地址偏移: 0x1A */ uint32_t LCKR; /*GPIO 配置锁定寄存器 地址偏移: 0x1C */ uint32_t AFR[2]; /*GPIO 复用功能配置寄存器 地址偏移: 0x20-0x24 */ } GPIO_TypeDef;这段代码用 typedef 关键字声明了名为 GPIO_TypeDef的结构体类型,结构体内有 8个成员变量,变量名正好对应寄存器的名字。C语言的语法规定,结构体内变量的存储空间是连续的(这一点非常重要,不然就乱了套了),其中 32 位的变量占用 4个字节,16位的变量占用 2 个字节,具体见。)也就是说,我们定义的这个 GPIO_TypeDef ,假如这个结构体的首地址为 0x40021C00(这也是第一个成员变量 MODER的地址), 那么结构体中第二个成员变量OTYPER的地址即为 0x4002 1C00 +0x04 ,加上的这个 0x04 ,正是代表 MODER所占用的4 个字节地址的偏移量,其它成员变量相对于结构体首地址的偏移,在上述代码右侧注释已给出,其中的 BSRR寄存器分成了低 16位 BSRRL和高 16位 BSRRH,BSRRL置 1 引脚输出高电平,BSRRH 置 1引脚输出低电平,这里分开只是为了方便操作。这样的地址偏移与 STM32 GPIO外设定义的寄存器地址偏移一一对应,只要给结构体设置好首地址,就能把结构体内成员的地址确定下来,然后就能以结构体的形式访问寄存器了,具体见代码GPIO_TypeDef * GPIOx; //定义一个 GPIO_TypeDef 型结构体指针 GPIOx GPIOx = GPIOH_BASE; //把指针地址设置为宏 GPIOH_BASE 地址 GPIOx->BSRRL = 0xFFFF; //通过指针访问并修改 GPIOH_BSRRL 寄存器 GPIOx->MODER = 0xFFFFFFFF; //修改 GPIOH_MODER 寄存器 GPIOx->OTYPER =0xFFFFFFFF; //修改 GPIOH_OTYPER 寄存器 uint32_t temp; temp = GPIOx->IDR; //读取 GPIOH_IDR 寄存器的值到变量 temp 中这段代码先用 GPIO_TypeDef类型定义一个结构体指针 GPIOx,并让指针指向地址GPIOH_BASE(0x4002 1C00),使用地址确定下来,然后根据 C语言访问结构体的语法,用GPIOx->BSRRL、GPIOx->MODER及 GPIOx->IDR 等方式读写寄存器。最后,我们更进一步,直接使用宏定义好 GPIO_TypeDef类型的指针,而且指针指向各个 GPIO端口的首地址,使用时我们直接用该宏访问寄存器即可./*使用 GPIO_TypeDef 把地址强制转换成指针*/ #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE) #define GPIOB ((GPIO_TypeDef *) GPIOB_BASE) #define GPIOC ((GPIO_TypeDef *) GPIOC_BASE) #define GPIOD ((GPIO_TypeDef *) GPIOD_BASE) #define GPIOE ((GPIO_TypeDef *) GPIOE_BASE) #define GPIOF ((GPIO_TypeDef *) GPIOF_BASE) #define GPIOG ((GPIO_TypeDef *) GPIOG_BASE) #define GPIOH ((GPIO_TypeDef *) GPIOH_BASE) #define GPIOI ((GPIO_TypeDef *) GPIOI_BASE) #define GPIOJ ((GPIO_TypeDef *) GPIOJ_BASE) #define GPIOK ((GPIO_TypeDef *) GPIOK_BASE)这里我们仅是以 GPIO 这个外设为例,给大家讲解了 C 语言对寄存器的封装。以此类推,其他外设也同样可以用这种方法来封装。好消息是,这部分工作都由固件库帮我们完成了,这里我们只是分析了下这个封装的过程,让大家知其然,也只其所以然。下面写一个小程序,其他的都已经省略:GPIO_InitTypeDef GPIO_InitStructure;//GPIO_InitTypeDef是一个结构体,GPIO_InitStructure是定义的一个结构体变量,你可以起名字为阿猫阿狗都可以,只不过我们习惯用GPIO_InitStructure,起到见明知意的效果,过有人都看得懂。 /* 第1步:打开GPIOA时钟,必须的一步 */ RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); /* 第2步:配置所有的按键GPIO为浮动输入模式(实际上CPU复位后就是输入状态) */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; /* PA13 */ GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN; /* 设为输入口 */ GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; /* 设为推挽模式 */ GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; /* 无需上下拉电阻 */ GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; /* IO口最大速度 */ GPIO_Init(GPIOA, &GPIO_InitStructure); /* 第1步:打开GPIOA的第13个引脚置位操作 */ GPIO_WritePin(GPIOA,GPIO_PIN_13,GPIO_PIN_SET); //引脚PA13拉高,置位1 GPIO_WritePin(GPIOA,GPIO_PIN_13,GPIO_PIN_RESET); //引脚PA13拉低,置位0

项目分享|小师弟手把手教你用蓝牙模块

一 、模块简介嵌入式蓝牙串口通讯模块(简称蓝牙模块)具有两种工作模式:命令响应工作模式和自动连接工作模式。当模块处于命令响应工作模式(或者AT模式)时能才能执行 AT 命令,用户可向模块发送各种 AT指令,为模块设定控制参数或发布控制命令。(AT指令就是我们PC与一些终端设备(例如蓝牙,WiFi模块)之间进行通信的,配置这些终端设备参数的一套指令。)在自动连接工作模式下模块又可分为主(Master)、从(Slave)和回环(Loopback)三种工作角色。当模块处于自动连接工作模式时,将自动根据事先设定的方式连接的数据传输。主模式:该模块可以主动搜索并连接其它蓝牙模块并接收发送数据。从模式:只能被搜索被其它蓝牙模块连接进行接收发送数据。回环:蓝牙模块就是将接收的数据原样返回给远程的主设备。二、开发工具1.USB转TTL模块(可用CH340),蓝牙模块(HC-05),PC端串口助助手。2.蓝牙模块与USB转TTL模块的接线情况就是VCC-VCC,GND-GND,RXD-TXD,TXD-RXD,有的蓝牙模块只有四个引脚,而且我们见到的大多数蓝牙模块都是四个或者只需要用到四个引脚。3.手机需要安装一个可以进行蓝牙通信的APP,一般直接在你手机的应用商店搜蓝牙串口就可以下载相关APP,如“串口调试助手”。三、蓝牙模块初始化这里以蓝牙模块HC-05为例,蓝牙模块可能每个人买到的不一样,但是用法都相近,可以作为参考,建议你在哪里买的模块就找对应的卖家找到对应的蓝牙模块中文数据手册,一般来说里面都有对应的AT指令集。这里要注意的是如果你发送对应的指令,在硬件连接都正确的情况下如果串口调试助手不能得到回应,那么很可能是你的AT指令集是错的,因为不同的蓝牙模块对应的蓝牙AT指令集是有差别的。HC-05蓝牙模块引脚说明:蓝牙模块自带一个状态指示灯1.在上电时,将KEY脚悬空或接地,红灯1s一次快速闪烁,表示进入可配对模式。2.在上电之前,按住黑色小设置按钮不放,同时KEY接高电平,上电,灯2s一次慢速闪烁,表示进入AT模式,进入AT模式可以对蓝牙模块进行设置。3.配对成功模式,快速闪烁。这三点很重要,蓝牙模块只有进入了AT模式才能发送AT指令集,很多小白以为只要接上USB转TTL模块插到电脑,打开串口调试助手就可以发送指令。这是不可以的。切记!!!## 四、单片机串口程序这里以51单片机为例,蓝牙串口的程序最重要的就是配置串口定时器,这里将定时器相关的代码贴出来,大家最好按照这种方法配置,经过多次建议程序稳定。### 串口初始化```bashvoid Usartinit()//初始化{    TMOD=0x21;  //设置定时器1的工作方式2   0010 0000  GATE C/T 8位初值自动重装8位定时器    PCON=0x80;  //波特率加倍     电源管理寄存器    TH1=0xf3;   //给定时器重装初值    TL1=0xf3;   //这里的波特率必须加倍  不加倍的话 通讯不成功 本人目前还不知道原因                //由于开发板使用的晶振频率是12M,非标准频率,在设置波特率时很容易产生误差,而导致串口通信出现乱码或者失败                //目前来说,选择波特率4800,SMOD=1波特率加倍的方式,误差率仅为0.16%,为12M晶振中最小的误差    TH0=0XEC;   //5ms定时        TL0=0X78;      TR0=1;      ET0=1;    TR1=1;      //开定时器1    SM0=0;      //选择串口工作方式1,常用    SM1=1;      REN=1;     //开串口接收   此时接收器所选择的波特率16倍速率采样RXD移交的电平 开始接收信息    ES=1;      //串行中断总开关    EA=1;      //开总中断}```相关配置程序注释已相当明白。### 主函数```bashuchar flag,i,receive;uchar code table1[]="GO";uchar code table2[]="Stop";void main(){     Usartinit(); //调用初始化函数进行初始化     while(1)     {       if(flag==1)     //不断的检测标志位是否被置1  被置1说明已经执行了中断服务程序,即已经接数据,否则一直检测flag的状态      {          switch(receive)          {            case 1:                          ES=0;  //接下来要发送数据 先要使ES=0关闭串口中断 等数据发送完后再打开串口中断                    for(i=0;i<2;i++)                    {                        SBUF=table1[i];                        while(!TI);   //等待是否发送完成    因为发送完成后TUI会有硬件置1                        TI=0;           //清除发送完成标志位  手动清0                    }                          ES=1;                                    flag=0;                            break;            case 2:                      ES=0;                    for(i=0;i<4;i++)                    {                        SBUF=table2[i];                        while(!TI);                        TI=0;                      }                          ES=1;                    flag=0;                        break;                            }               }   }    }void Usart() interrupt 4 //一旦有数据接入,串行口中断触发{   receive=SBUF-48;     //当REN为1时  开始接收数据 将接收到的值赋予receive   这里的是ACSII 所以要减去48   RI=0;                //当RI=0   将接收数据存入SBUF寄存器中  清除接收中断标志位  有内部硬件置1,项CPU发出中断请求 在中断服务程序中,必须用软件将其清零, 取消此中断申请   flag=1;              //将标志位置1  这个是方便在主程序中查询判断是否已经接收到数据}```以上是单片机的程序,由于51单片机只有一对RXD和TXD引脚,故先将程序下载到单片机后再将蓝牙模块的四个引脚接到单片机的RXD和TXD引脚,以后每次下载程序是都要这样操作,但不要嫌麻烦。五、手机端操作在安卓手机的应用商店搜索“蓝牙调试助手”,我的应用商店下载的是“蓝牙调试器”在没有连接成功蓝牙时,蓝牙上面的红灯一直在闪烁,当连接成功后,红灯停止闪烁,每发送一条指令,电脑的串口调试助手就会收到手机端发送的消息。现在,你会用蓝牙模块了吗?项目分享|小师弟手把手教你用蓝牙模块

一个由C/C++编译的程序占用的内存分为以下几个部分 * 栈区(stack)— **由编译器自动分配释放,存放函数的参数值,局部变量的值等**。 * 堆区(heap) — **由程序员分配和释放,若程序员不释放,程序结束时可能由OS回收**。 * 全局区(静态区)(static)—**全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量、未初始化的静态变量在相邻的另一块区域**。 * 文字常量区 — **常量字符串就是放在这里的**。 * 程序代码区 — **存放函数体的二进制代码**。
我们知道彩色位图是由R/G/B三个分量组成,其文件存储格式为 BITMAPFILEHEADER+BITMAPINFOHEADER,紧跟后面的可能是: * 如果是24位真彩图,则每个点是由三个字节分别表示R/G/B,所以这里直接跟着图像的色彩信息; * 如果是8位(256色),4位(16色),1位(单色)图,则紧跟后面的是调色板数据,一个RGBQUAD类型的数组,其长度由BITMAPINFOHEADER.biClrUsed来决定。 然后后面紧跟的才是图像数据(24位图是真实的图像数 利用C语言读取BMP文件
BMP是bitmap的缩写形式,bitmap顾名思义,就是位图也即Windows位图。它一般由4部分组成:文件头信息块、图像描述信息块、颜色表(在真彩色模式无颜色表)和图像数据区组成。在系统中以BMP为扩展名保存。   打开Windows的画图程序,在保存图像时,可以看到三个选项:2色位图(黑白)、16色位图、256色位图和24位位图。这是最普通的生成位图的工具,在这里讲解的BMP位图形式,主要就是指用画图生成的位图.   一般的bmp图像都是24位,也就是真彩。每8位为一字节,24位也就是使用三字节来存储每一个像素的信息,三个字节对应存放r,g,b三原色的数据每个字节的存贮范围都
1)程序结构是三种: 顺序结构 、选择结构(分支结构)、循环结构。 2)读程序都要从main()入口, 然后从最上面顺序往下读(碰到循环做循环,碰到选择做选择),有且只有一个main函数。 3)计算机的数据在电脑中保存是以二进制的形式. 数据存放的位置就是 他的地址. 4)bit是位 是指为0 或者1。 byte 是指字节,**一个字节 = 八个位**
51单片机原理以及接口技术(三)-80C51的指令系统
指令是CPU按照人们的意图来完成某种操作的命令。一台计算机的CPU所能执行全部指令的集合称为这个CPU的指令系统。**指令系统功能的强弱决定了计算机性能的高低**。 80C51单片机具有111条指令,其指令系统的特点为: (1)**执行时间短。1个机器周期指令有64条,2个机器周期指令有45条,而4个机器周期指令仅有2条**(即乘法和除法指令); (2)指令编码字节少。**单字节的指令有49条,双字节的指令有45条,三字节的指令仅有17条**; (3)位操作指令丰富。这是80C51单片机面向控制特点的重要保证。
51单片机原理以及接口技术(二)-单片机结构和原理
Intel公司推出的MCS-51系列单片机以其典型的结构、完善的总线、特殊功能寄存器的集中管理方式、位操作系统和面向控制的指令系统,为单片机的发展奠定了良好的基础。 8051是MCS-51系列单片机的典型品种。众多单片机芯片生产厂商以8051为基核开发出的CHMOS工艺单片机产品统称为80C51系列。
51单片机原理以及接口技术(一)-单片机发展概述
  ENIAC 是电子管计算机,时钟频率虽然仅有 100 kHz,但能在 1 s 的时间内完成 5 000 次加法运算。与现代的计算机相比,ENIAC 有许多不足,但它的问世开创了计算机科学技术的新纪元,对人类的生产和生活方式产生了巨大的影响。   在研制 ENIAC 的过程中,匈牙利籍数学家冯·诺依曼担任研制小组的顾问,并在方案的设计上做出了重要的贡献。1946 年 6 月,冯·诺依曼又提出了
CPU:STM32F103RCT6,LQFP64,FLASH:64KB,RAM:20KB flash起始地址为0x8000000,大小为0x10000(16进制)—>65536字节(10进制)—>64KB RAM起始地址为0x2000000,大小为0x5000(16进制)—>20480字节(10进制)—>20KB
2019年全国大学生电子设计大赛(简单电路特性测试仪)
本测试仪以低功耗STM32F407单片机为核心控制器件,利用DDS芯片AD9850和电阻分压电路产生1kHz 30mV正弦信号输入到待测电路,通过采集输入电压Ui和流过输入电阻上电流Ii,算出输入电阻Ri;采集开路电压Uopen和带载电压U1并结合流过负载电阻上的电流Io,算出输出电阻Ro;结合待测电路的工作特性来分析和判断待测电路的故障原因。   实测结果表明,该测试仪能正常输出1KH的正弦波,测得输入电阻为3.2K ohm,输出电阻为1.96K ohm,在输入1KHz正弦信号下,增益为20.3dB,扫描频率可清晰显示该放大电路的幅频特性曲线且测得上限频率为65KHz;改