开始之前推荐一个电路学习软件,这个软件笔者也刚接触。名字是Circuit有在线版本和不在线版本,这是笔者在B站看视频翻到的。
Paul Falstadhttps://www.falstad.com/这是地址。
离线版本在网站内点这个进去
根据你的系统下载你需要的版本红线的是windows版本。它的使用说明都是英文,如果是英文苦手,可以使用QQ浏览器自带网页翻译或者使用翻译工具百度或者搜狗,它有个文件翻译功能可以翻译整个文件。虽然有些翻译的不正确,总比没有的好。
这期浅述下C语言指针与串口通信的一些功能。在学C之前笔者就听过C指针的恶名,这次写程序的时候也确实狠狠的体验了一把。笔者的C也是初学,目前是51特攻。除了之前博文使用的语句外,笔者对C的其他用语句没有过多涉及或者说基本没有涉及。因此为了写这个程序笔者又在B站大学突击了一下,最终是完成了。程序是简单的,但是完成这个程序花了笔者不少时间。
言归正转:
变量的地址:
要研究指针,得先来深入理解内存地址这个概念。打个比方:整个内存就相当于一个拥有很多房间的大楼,每个房间都有房间号,比如从101、102、103一直到NNN。房间号就是房间的地址,相对应的内存中的每个单元也都有自己的编号,比如从0x00,0x01,0x02一直到0xNN,同样可以说这些编号就是内存单元的地址。房间里可以住人,对应的的内存单元里就可以"住进"变量了。
假如1位名叫A先生的顾客住进101的房间,就可以说101就是A先生的住址。同样一个名为x的变量住在编号为0x00的内存单元中,就可以说0x00是变量x的地址。
基本的内存单元是字节,英文单词为Byte。(通俗的讲是1个地址单元只能存储8位数据,就好比一间客房只能住1个人,而这个房间号或者地址名可以是8位、16位的或者32位的这个一般受限于地址总线,51单片机的地址总线是16根因此它的取值范围是0x0000-0xFFFF因此它的寻址空间最大是64KB,51单片机的内存RAM的地址范围是0x00-0xFF,也就是它只需8根地址总线寻址,且可拓展的外部RAM最大空间为64KB)
看图不同类型元素在地址中存储:
假设要存储char型的变量 a和b,因为a和b只有1个字节的大小,因此它只需要一个存储单元就能存下数据。 定义1个unsigned int型的变量c = 0x0A0B,它的大小有两个字节因此需要两个存储单元。那么变量C的低字节序内容0B,是存储在内存空间中的低地址还是高地址即如图是0x02还0x03?
这个问题是由所用C编译器与单片机构架共同决定的,单片机类型不同就有可能不同。Keil+51单片机的环境下,0x02存的是高字节内容,0x03存的是低字节内容0B。同理long型的d储存需要4个存储单元。这种存储方式在C语言中叫大端字节序。与之相反常用的VS编程软件储存方式是小端字节序。
标准的Big-Endian和Little-Endian的定义如下:
a) Little-Endian 小端
就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
b) Big-Endian 大端
就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
实际生活中,要寻找1个人有两种方式,一种是通过它的名字来找人,还有第二种方式就是通过它的住址来找人。在派出所的户籍管理系统中输入小明的家庭住址,系统就会自动指向小明的相关信息。那么在C语言中,要访问一个变量同样有两种方式:一种是通过变量名来访问,另一种自然就是通过变量的地址来访问。
在C语言,地址就等同于指针,变量的地址就是变量的指针。举个例子:
unsigned char a = 0;//定义了变量a
unsigned char* ptr = &a; //定义1个char型的指针变量,这个指针变量ptr的值是变量a的地址
虽然这个ptr指针指向的是变量a的地址,作为全局变量a程序开始运行后 编译系统会自动分配变量的地址,对于变量a来说这个地址是固定,但是对于指向变量的指针变量(地址)ptr它是可以被操作的,即左移或者右移。那么它移动一次的距离由指针类型决定的。比如上述的char*就是代表指针移动一次跨过一个存储单元,int*就代表指针移动一次跨过两个储存单元。注:这只在51单片机中是这样的,电脑端VS软件int型是4个字节的。
定义一个int型的变量如上图c=0x0A0B,它的存储单元有两个。那么它的指针变量是指向哪个存储单元?它指向的一定是该变量的低地址这是由C语言本身所决定的规则 ,即上图的0x02,变量c低地址0x02中存放是高字节内容0x0A。这和你是哪种指针类型没有关系,它不会因为你是int型的指针就指向两个地址(注:这里仅只描述地址的位置,它操作1次的范围还是两个存储空间,它执行的内容是地址本身的内容和地址+1后指向的内容),上图long型的变量d它的(ulong*p)指针指向的也是低地址0x04。
举个例子:变量B = 0x12345678 一个long型的变量
如果我们定义1个char*的指针变量ptr。
即 unsigned char * ptr = &B; 我们用printf打印这个 *ptr指向的内容,在小端序的主机中它的值是78H,如果用51单片机它是大端序它的值是12H.
如果我们定义1个int*的指针变量ptr
即 unsigned int * ptr = &B 我们用printf打印这个 *ptr指向的内容,在小端序的主机中它的值是5678H,如果用51单片机它是大端序它的值是1234H.
如果我们定义1个long*的指针变量ptr
即 unsigned int * ptr = &B 我们用printf打印这个 *ptr指向的内容,在小端序的主机中它的值是12345678H,如果用51单片机它是大端序它的值是12345678H.
**如果我们把变量B=0x12345678直接赋值给SBUF缓冲寄存器,不做其他的操作即只传输1次。问这个操作在串口助手STC-ISP接收缓冲区最后显示的值是什么?**后续笔者会给出完整的测试程序。
void main()
{
unsigned long B = 0x12345678;
unsigned char* p = &B;
unsigned char* ptr = &B;
unsigned long* ptr1 = &B;
SBUF = 0x12345678;
SBUF = B;
SBUF = *P;
SBUF = *ptr;
SBUF = *ptr1;
}
void interruptUART() interrupt 4
{
if(RI)
{
RI = 0;
}
if(TI)
{
TI = 0;
}
}
从题目上看着好像是个信息传递的问题?即对于一个多字节的的变量来说,它在传输的过程中是先传递低字节的内容呢,还是高字节的内容呢或者是先传输低地址的内容,还是高地址的内容呢?如果你涉及过着方面的内容肯定知道一些相关的内容可能在考虑什么网络传输字节序和主机传输字节序什么什么得。
先说结果:
SBUF = 0x12345678; 它最后在接收缓冲区的值是 78H
SBUF = B;它最后在接收缓冲区的值是 78H
从上面两个结果上来看它好像是先传输了低字节(即高地址)的内容。
char型指针 SBUF = *P; 它最后在接收缓冲区的值是 12H
int型指针 SBUF = *ptr; 它最后在接收缓冲区的值是 34H
long型指针 SBUF = *ptr1;它最后在接收缓冲区的值是 78H
可以看到用三种不同的指针它发送的结果都不一样。笔者陈述下这个问题,可能不是非常正确,只是笔者目前的理解。
1:对于串口软件STC-ISP,它的工作原则是我先接收到什么数据我就先显示什么数据,并且显示的数据在接收缓冲区里从左往右依次排序。即左边的数据接收到的时间是早于右边的数据:,它显示的最小单元是1个字节(即8为数据)。
2:SBUF它只是1个8位缓冲寄存器,因此对于51单片机如果你1次要传输多字节的内容,它仅会传输8位数据,后面的数据它就中止传输了抛掉了。而且传输的是该数据的低8位,有点像强制类型转换。如果说它是1个16位的缓冲寄存器对于变量B,SBUF = B,他的结果就会是5678H。
3:对于char型指针,它的操作权限只有一个字节,鉴于是大端序,因此该指针操作一次的内容是12H,刚好SBUF是8位寄存器可以完全的显示出来。
对于int型指针它的操作权限是两个字节,鉴于是大端序,因此该指针操作一次的内容是1234H,应用陈述2的结论它结果就34H。
对于longt型指针它的操作权限是4个字节,鉴于是大端序,因此该指针操作一次的内容是12345678H,应用陈述2的结论它结果就78H。
因此对于51单片机我们在传输的时候要控制每次传输的内容是1个字节,不要超出了。而且无论它是用什么协议的,程序是我们自己设计,作为程序员的我们自己知道先发送的是哪些内容。
指向数组元素的指针
所谓指向数组元素的指针,其本质还是变量的指针,因为数组中的每个元素,其实都可以直接看成是1个变量 ,所以指向数组元素的指针,也就是变量的指针。
定义1个数组元素
unsigned char number[10] ={0,1,2,3,4,5,6,7,8,9}; unsigned char *p = &number[0];它也可以写成
unsigned char* p = number; 数组元素名其实就代表了数组元素的首地址。整个数组元素是在内存空间中连续存储的,那么首元素是存在高位第地址还是低位地址呢?对于数组、字符串来说首元素就相当于高字节。
数组、字符串或者文本它们的首元素都是存在低位地址的,无论是大端序还是小端序,它和单纯的变量存储是不一样的的。(这个是笔者暴论可能也存在不一样的,PC主机VS软件是小端序但它的首地址也是放在低位的,因为大端序是符合我们这种自然人阅读习惯的)
- 1:定义1个Int型变量L=0x1134;
定义1个int的指针指向这个变量unsigned int*p = &l;
然后再定义1个char型的变量K ,最后把指针P指向的内容存入K中 k = *p,问K的值是多少?
结果 **K = 0x34;**这其实是强制类型转换了把int的变量存入char型的变量地址,它只存入低字节的内容和大小端序没什么关系。
- 2:还是这个变量,把这指针变量改成char型unsigned char*p = &l; 问现在K的值是多少?
结果K = 0x11;char型指针指向的是变量L的低位地址,低位地址存储的是0x11,加上char型指针的权限因此结果是0x11。
- 3还是这个变量指针变量依然是char型,现在把K定义为1个Int型的数,问现在K的值是多少?
结果K=0x0011;11H存放在高位地址,因为51单片机是大端序。
那么定义1个数组unsigned char array4[8] ={0x1,0x23,0x123,0x1234,0x12345,6,7,8};首先可以看到很多数组元素已经不符合数组定义的类型了。看下代码:
#include <reg52.h>
bit cmdArrived = 0; //命令到达标志,即接收到上位机下发的命令
unsigned char cmdIndex = 0;
unsigned char *ptrTxd;
unsigned char cntTxd = 0;
unsigned char array1[1] = {1};
unsigned char array2[2] = {1,2};
unsigned char array3[4] = {0x11,0x21,0x31,0x41};
unsigned char array4[8] ={0x1,0x23,0x123,0x1234,0x12345,6,7,8};
unsigned int l =0x1134;
unsigned char*p = &l;
unsigned int k ;
void ConfigUART(unsigned int baud);
void main()
{
EA = 1; //开总中断
k = *p;
ConfigUART(9600); //配置波特率为9600
while (1)
{
if(cmdArrived)
{
cmdArrived = 0;
switch(cmdIndex)
{
case 1:
ptrTxd = array1;
cntTxd = sizeof(array1);
TI = 1;
break;
case 2:
ptrTxd = array2;
cntTxd = sizeof(array2);
TI = 1;
break;
case 3:
ptrTxd = array3;
cntTxd = sizeof(array3);
TI = 1;
break;
case 4:
ptrTxd = array4;
cntTxd = sizeof(array4);
TI = 1;
break;
default:
break;
}
}
}
}
void ConfigUART(unsigned int baud)
{
SCON = 0x50; //配置串口为模式1
TMOD &= 0x0F; //清零T1的控制位
TMOD |= 0x20; //配置T1为模式2
TH1 = 256 - (11059200/12/32)/baud; //计算T1重载值
TL1 = TH1; //初值等于重载值
ET1 = 0; //禁止T1中断
ES = 1; //使能串口中断
TR1 = 1; //启动T1
}
void InterruptUART() interrupt 4
{
if (RI) //接收到字节
{
RI = 0; //清零接收中断标志位
cmdIndex = SBUF;
cmdArrived = 1;
}
if (TI) //字节发送完毕
{
TI = 0; //清零发送中断标志位
if(cntTxd > 0)
{
SBUF = *ptrTxd;
cntTxd--;
ptrTxd++;
}
}
}
这个代码的功能就是自动计算数组元素的长度,并把数组元素1个个发送到串口助手的接收区显示出来。操作方式就是在单字符串发送区用16进制发送数字2,对函数来说就是使能了case 2:
因此显示出来的数据是数组2的元素。sizeof() 是一个判断数据类型或者表达式长度的运算符。
问如果发送数字4显示的数组元素是什么?
原数组 :unsigned char array4[8] ={0x1,0x23,0x123,0x1234,0x12345,6,7,8};结果是:
:
不知道这个结果是否符合读者你们的计算结果。首先呢这是char类型的数组,如果它存入的数据所占的内存空间大于char的内存空间,它会先发生强制类型转换,对于这个数组来说它就只存8位数多余的就会被抛掉,然后再传输数据因此这个强制类型转换后的结果是:
**unsigned char array4[8] ={0x1,0x23,0x23,0x34,0x45,6,7,8}**这就是串口助手显示出来的数据。
问:如果把数组的类型改成int型,即 unsigned int array4[8] ={0x1,0x23,0x123,0x1234,0x12345,6,7,8}那么最后显示出来的结果是什么?
首先它会报警,提示指向不同对象的指针,不过不影响工作。
结果是这个,我们分析下为什么会是这个结果。首先数组类型从char变成了int型,代表着它的存储空间扩大了1倍,因此sizeof()函数计数的结果翻倍了,即cntTxd的值是原先的2倍。所以它发送的值变成了16个,又因为是char型的指针因此它操作1次的内容是1个字节,操作1次越过的地址也是1个,因此它的结果就是把数组里的元素按照地址从低到高全部发出来了,因为数组所占的地址刚好是16个。并且对于long型的0x12345它显示的数是23H,45H,这个转换结果也符合之前的陈述。
问:如果把ptrTxd指针改成int型,数组也是Int型,它显示的结果会是什么?
不知道这个结果是不是符合你的推论,显然前8个的显示结果和第一次的显示结果是一样的。我们分析一下,因为该数组还是int型的,因此数组求出的cntTxd值依然是16是之前两倍.int型的指针1次操作的内容是两个字节且跨过的地址也是两个,前面例子已经展示过SBUF它是个8位的寄存器,因此它只传输了低字节的内容。而操作8次就到达了数组的边界,因而后面8次的内容就都是数组之外连续的高位地址存储的内容。这就越界了,指针也变成了野指针。从这里你就可以看到指针是非常的自由,同样的用好它你就需要注意每个细节。如果我们不越界计数这里除以2,它就不会越界了,即对char类型的元素sizeof()函数求出的值刚好是元素个数,对与其他类型的元素的值的应用,应该要配合指针的类型来确定。
字符数组和字符指针
在程序运行过程中,其值不能被改变的量称之为常量。常量分为不同类型,有整型常量如1、2、3;浮点型常量3.14、0.56、-4.8;字符型常量'a','b','1'用单引号括起来,字符串常量"abcd","1234","abc123",用单引号括起来的表示1个字符,用双引号括起来的是1个字符串。
字符串常量是常量不能被修改的,因此如果你要对字符串处理应该开辟字符数组。
字符串常量在内存中按顺序逐个储存字符串中字符的ASCII码值,并且特别注意,最后1个字符'\0',它是字符串的结束标志,它是系统自动添的。也就是说"abcd"这个字符串有5个字符,但是对于字符数组char arr[] = {'a','c','c','d'};系统是不会自动添加'\0'的。在字符串中空格也是字符,因此要注意。
笔者前面的博文有一个关于电子密码锁的功能,密码是2024 键码(区别长短按键)是1010。如果想要通过串口通信交互改掉密码,必然面对一些问题:如何识别输入的数据是用于修改密码的。对此程序内要有明确的规定。
因此使能这个功能的格式是 :先输入指令码 再输入数据码
因此必然要对指令码进行区别,这个指令码是干什么的呢?正常状态下程序是锁定密码修改的,为此必须给他一个指令告诉它打开修改密码的功能。这个指令可以是数字,字母,字符串只要和程序约定好都行。单个数字,字母这些虽然都可以但是不太符合自然人使用习惯,而且容易误触。因此使用字符串会是一个比较好的方式。
比如我就设定1个字符串"setkey"作为密码锁开启的指令,那么怎么在串口通信中实现这个功能呢?:
- 1:需要开辟一个内存空间用于存储6个字节,因此可以开辟1个char的数组arrbuf[6],这个可以用于存放通过串口通信传输过来的数据。
- 2:程序怎么辨别接收的字符串已经发送完毕了?C语言在操作字符串的时候它会自动加字符串结束码'\0',很多库函数在操作字符串的时候都是以这个字符来判断的。但是**串口通信助手STC-ISP软件,在"单字符串发送区"向单片机发送字符串的时候它是不会自动加'\0',因此需要手动添加字符串结束码,本案添加的是!。**即设定的开启指令是"setkey!",当然你也可以不添加,直接以字符串最后的字符y作为结束码判断,但是这个代码扩展性就低了,也不怎么规范。
- 3:很多字符串的库函数工作方式都是通过判断字符串结束码'\0'来实现功能的,因此一开始开辟的存储空间需要扩大,最少7个,多开辟几个也是没问题的。本案采用的strcmp(),字符串比较函数。这样就能确定输入的字符串是不是预设的指令了。这就完成了指令部分
- 4:字符串怎么变成整数,如"2024"字符串变成整数2024,本案采用的还是库函数atoi()来实现,当然这个也是要加入结束码!。如此就是实现了密码修改的最初要求,数据的正确的获得。获得这些值后,应该是可以通过修改下程序功能实现串口通信修改密码的。
看代码:
# include<reg52.h>
# include<string.h>
# include<stdlib.h>
bit cmdArrived = 0;
unsigned char cmdindex = 0; //命令索引,即与上位机约定好的数组编号
unsigned char cntTxd = 0; //串口发送计数(数组字节个数)
unsigned char *ptrTxd = 0; //串口发送指针
unsigned char array1[] = "1-Hello!\r\n";
unsigned char array2[] ={'2','-','H','e','l','l','o','!','\r','\n'};
unsigned char array3[] = {51,45,72,101,108,108,111,33,13,10};
unsigned char array4[] = "4-Hello!\r\n";
unsigned char ErroyRpt[] = "Error!Please re-enter\r\n";
pdata unsigned char CorrectRpt[] = "Correct!Please enter a 4-digit password";
unsigned char WordSet[] = "setkey";
unsigned char* wd_set = WordSet;
pdata unsigned char Word[7] = {'2','0','2','4','\0','s','b'};
pdata unsigned long RxdByte = 0x12345678;
pdata unsigned int* y = &RxdByte;
unsigned char *ptrRxd = Word; //串口接收指针
bit Rxd_Fin_Mark = 0; //接收接收位标志
unsigned int vaule = 0;//atoi(Word);
unsigned char* p = &vaule;
unsigned char x = 0;
code unsigned char* k = &vaule;//备份指针地址,用于指针初始化
pdata unsigned long l = 0x010203;
unsigned char* s = &l;
void ConfigUART(unsigned int baud);
void main()
{
EA = 1;
ConfigUART(9600);
//strcpy(array2,"2-Hello!");
while(1)
{
if(strcmp(WordSet,Word) == 0 && Rxd_Fin_Mark == 1)
{
Rxd_Fin_Mark = 0;
ptrRxd = &Word[0];
cmdArrived = 1;
cmdindex = 8;
}
if(cmdArrived)
{
cmdArrived = 0;
switch(cmdindex)
{
case 1:
ptrTxd = array1; //数组1的首地址赋值给发送指针
cntTxd = sizeof(array1); //数组1的长度赋值给发送计数器
TI = 1; //手动方式启动发送中断,处理数据发生
break;
case 2:
ptrTxd = array2;
cntTxd = sizeof(array2);
TI = 1;
break;
case 3:
ptrTxd = array3;
cntTxd = sizeof(array3);
TI = 1;
break;
case 4:
ptrTxd = array4;
cntTxd = sizeof(array4) -1 ;
TI = 1;
break;
case 5:
// ptrTxd = ErroyRpt;
//cntTxd = sizeof(ErroyRpt) ;
SBUF = *y;
// TI = 1;
break;
case 6:
ptrTxd = Word;
cntTxd = sizeof(Word);
TI = 1;
break;
case 7:
vaule = atoi(Word);
cntTxd = sizeof(vaule);
ptrTxd = k;//指针初始化
TI = 1;
break;
case 8:
ptrTxd =CorrectRpt;
cntTxd = sizeof(CorrectRpt) ;
TI = 1;
break;
case 9:
Rxd_Fin_Mark = 1;
default:
break;
}
}
}
}
/*串口配置函数 buad为通信波特率 */
void ConfigUART(unsigned int baud)
{
SCON = 0x50;
TMOD &= 0x0F;//定时器控制寄存器,T1区域清零,T0区域不动
TMOD |=0x20;//工作在模式2
TH1 = 256 - (11059200/12/32)/baud;
TL1 = TH1;
ET1 = 0; //禁止使能T1中断
ES = 1; //使能串口中断
TR1 = 1;
}
/*UART中断服务函数 */
void interruptUART() interrupt 4
{
static unsigned char i = 0; //定义数组指针在数组的索引
if(RI)
{
if(SBUF>= 0 && SBUF<=9) //输入0-9输出相应的语句
{
cmdindex = SBUF; //把数据赋值给命令索引
cmdArrived = 1; //使能命令语句后后续的数据传输就停止使能
}
if(SBUF == 32) //如果输入的是空格键则指针回到数组的首地址
{
i = 0; //初始化
ptrRxd = Word;
}
else //不是上述的输入都存入word数组,数组最大储存6个字节的数据
{
if(cmdArrived == 0)
{
//if((SBUF >='0' && SBUF <= '9' ) | SBUF == '!')
if(SBUF == '!')
{
*ptrRxd ='\0'; //把字符串结束位符号发给相应的数组地址
ptrRxd = Word;//初始化
i = 0;
Rxd_Fin_Mark = 1;
}
else
{
* ptrRxd = SBUF; //把数据存入指针指向的地址区域
ptrRxd++; //指针数加1
i++; //指针索引加1
}
if(i >= 7) //这里不能用ptrRxd >=6判断因为这是数组地址它的值不是从0开始的
{ //这里i的值等于Word数组的长度,数组内存扩充了注意更改。
i = 0;
ptrRxd = Word;//初始化
}
}
}
RI = 0;
}
if(TI)
{
if(cntTxd > 0) //发送的数据判断
{
SBUF = *ptrTxd; //把相应地址的数据发送
cntTxd--; //每发送一个数据计数减1
ptrTxd++;//每发送1个数据相应的地址移动1位
}
TI = 0;
}
}
写的有点乱,不过这本身就是个测试程序,有些变量可能无用了,是之前笔者测试别的用的。如果有兴趣可以拷贝过去自己测试一下,这个程序使用的时候有几个注意点。
1:程序烧录完成后,必须要关机再开机,测试程序才正常,因为在烧录过程中,就会向串口写入一堆其他的数据,因此要想正确使用,必须关机再开机就正常了。
2:
1:输入16进制的1-9 根据你想要的结果,用16进制显示或者字符显示。
2:要想输入字符串就用字符格式发送。
3:字符串结尾要加上!。
4:如果在使用过程中找不到指针位置,可以输入字符'!'或者字符'空格'来使指针初始化位置,
主意:输入空格的结果只是让光标移了一位但是确实是输入了空格字符,然后点发送字符/数据按钮指针就初始化,执向数组的首元素了。
5:这个函数的指令长度不一定要7位的,它是不固定的。你预设设置为set!这也是可以实现的,因为库函数在使用的时候遇到'\0'就结束后,后面是什么字符它不管的。所以这就很方便扩展移植。
然后向分享1个关于kei1,memery窗口的相关情况。如果有小伙伴知道这个原因,能否留言告诉我一下。keil5memery内容查找_哔哩哔哩_bilibili
测试程序操作示意:
对于这次程序笔者总结了一点心德:
1:在使用指针的时候要注意是否要初始化,要时刻注意指针的边界问题,以及类型问题。
2:对于串口通信发送端:在发送数据前要指针初始化。
3:对于串口通信接收端:在接收完字符串数据指针要初始化。
4:字符串结束码是个好标志。