单片机C语言基础
字符串含中文报错
文件格式问题,之前遇到过,用 TXT打开那个含中文字符串的 .c 文件如果是 UTF-8
需要改成 ANSI
即可
结构体初始化
需要注意,如果要初始化结构体里的数组需要 {0},不能直接0,这样只能初始化第一个元素而后面的元素则没有被初始化
最好初始化时加上 .成员
来初始化这样可以避免因为顺序错误而导致的编译错误,注意 .
前面不要加类型
//例子
typedef struct
{
int a,
char arr[3],
}DATA_TypeDef;
DATA_TypeDef Data =
{
.a = 0,
.arr = {0}
};
附1:Error: Cannot load driver ‘C:\Keil v5\ARM\Segger\JL 2CM3. dll’
解决方案有两种:
- 将keil安装目录的Segger路径,如D:\Keil_v5\ARM\Segger添加到系统环境变量(对我来说没起作用)
- 用另一部电脑测试不报错的Sergger包覆盖我的电脑下Keil安装目录下的Sergger文件夹(亲测有用)
而且我发现这个搞好打开工程不会闪退了之前打开某个工程一直闪退
关于static
函数加static的作用是 隐藏
,加了static,就会对其它源文件隐藏,利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突
如果是变量加static,static变量存放在静态存储区,所以它具备持久性和默认值0,比如把一个字符串设为static则不需要每次在末尾添加\0
局部变量加static作用
—static修饰局部变量时,其实改变的是局部变量的存储位置,原来的局部变量放在栈区,静态的局部变量是放在静态区,放在静态区的变量出了作用域是不会销毁的,相当于生命周期延长了(可以不同文件调用)
全局变量加static作用
—static修饰全局变量时,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是extern外部声明也不可以(不可以不同文件调用)
函数加static作用
—static修饰一个函数,则这个函数的也只能在本文件中调用,不能被其他文件调用,相当于隐藏了(不可以不同文件调用)
关于 # if…# endif
```cpp
//这样相当于注释,在它们之间的内容就会被屏蔽,不会执行;
# if 0
printf("测试\n");
# endif
//这样相当于成立,一定会执行
# if 1
printf("测试\n");
# endif
//这样相当于开关,执行if不会执行else
# if 1
printf("你好\n");
# else
printf("大家好\n");
# endif
//这样相当于开关,执行else不会执行if
# if 0
printf("你好\n");
# else
printf("大家好\n");
# endif
```
memcpy
头文件
:
#include <stdio.h>
#include <string.h>
memcpy
可用于整型数组的复制
arr1[4] = {1,2,3,4};
arr2[4] = {0};
memcpy(arr2,arr1); //把arr1复制到arr2里
宏定义拼接符
在宏定义里, ##
表示拼接符,它的作用是将两个符号拼接成一个符号,不能拼接两个字符串
//例如
#define CONCAT(a,b) a##b
int result = CONCAT(10,20); //则结果是1020
volatile用法
volatile uint32_t CPU_RunTime = 0UL;
0UL
表示一个无符号长整型常量,其值为零。UL 的意思是 无符号长整型
,它强制将常量解释为无符号长整型,而不是默认的整型。 使用 UL 后缀可以确保常量的类型与变量的类型匹配,以避免类型转换错误
。
因为它是一个 volatile 变量,所以编译器 不会对它进行优化
, 以确保每次读写变量时都能够访问最新的值
。这样可以避免由于编译器优化导致的运行时间计数器不准确的问题。
常用C库
atoi
函数:把字符串转化为整型(int),头文件stdlib.h
它返回值是int类型
如果字符串首元素不是空格字符:1.如果第一个字符不是数字字符,直接返回0。2.如果第一个字符是数字字符, 则从这个数字字符开始转换,并向后找连续的数字字符转换 ,如果连续中断,找到不是数字字符的字符,则在此截断寻找,返回前面已经转换好的连续的数字字符字面整型值。
itoa
函数:把整型转化为字符串存储在数组
itoa(整型数据,存储目标字符串的数组,想要结果的进制)
字符转正常数字
有时候要在字符串或者字符数组里提取里面的整型数字,可以直接 -'0'
简单来说:数字转字符:+‘0’ 字符转数字:-‘0’
char arr[2] = "23";
char a;
a = arr[0] - '0'; //结果a=2
b = arr[1] - '0'; //结果a=3
判断是不是ASCII
可以通过C语言库:isascii()
函数,需要包含头文件 ctype.h
,或者判断ASCII字符编号,可显示的字符只有 32~126
,所以可以这样:
if(arr[0] >= 32 && arr[0] <= 126)
{
//表示是字符
}
else
{
//不是字符
}
还有就是 0~9
,a~z
,A~Z
可以直接加个单引号’'来判断不需要判断它的字符编号
浮点数存储规则
参考文章:
任意一个二进制浮点数 V 可以表示成下面的形式:
其中,
表示 符号位
,当,V为 正数
;当,V为 负数
M表示有效数字,
表示 指数位
对于 32位
的浮点数(单精度浮点数),最高的1位是符号位 s
,接着的8位是 指数E
,剩下的23位为 有效数字M
对于 64位
的浮点数(双精度浮点数),最高的1位是符号位s
,接着的11位是 指数E
,剩下的52位为 有效数字M
对有效数字 M 和指数 E ,还有一些特别规定
M 可以写成 1.xxxxxx
的形式,其中 xxxxxx 表示小数部分
,默认这个数的第一位总是 1 ,因此 可以被舍去,只保存后面的xxxxxx部分
E
为一个无符号整数( unsigned int ),这意味着,如果 E 为 8 位,它的取值范围为 0~255
;如果 E 为 11 位,它的取值范围为 0~2047
,所以规定 存入内存时 E 的真实值必须再加上一个中间数
,对于 8
位的 E,这个中间数是 127
;对于 11
位的 E ,这个中间 数是 1023
然后,指数E从内存中取出还可以再分成三种情况
情况 | 描述 |
---|---|
E 不全为 0 或不全为 1 | 即 指数E的计算值减去 127 (或 1023 ),得到真实值,再将 有效数字M 前加上第一位的 1 |
E 全为 0 | 指数 E 等于 1-127 (或者 1-1023 )即为真实值, 有效数字M 不再加上第一位的 1 ,而是还原为 0.xxxxxx 的小数 |
E 全为 1 | 如果有效数字 M 全为 0 ,表示 ± 无穷大(正负取决于符号位 s ) |
最终结果是:s + E + M
比如:231.5
//单片机的应用
union ee_float
{
float value;
uint8_t buffer[4];
}float_write,float_read;
//231.5在内存里的存储是0x43678000
float_write.buffer[0] = 0x00;
float_write.buffer[1] = 0x80;
float_write.buffer[2] = 0x67;
float_write.buffer[3] = 0x43;
for(i=0;i<sizeof(float);i++)
{
eeprom_write_byte(float_write.buffer[i],0x00+i);
HAL_Delay(5);
}
for(i=0;i<sizeof(float);i++)
{
float_read.buffer[i] = eeprom_read_random(0x00+i);
}
printf("测试\r\n");
printf("%f\r\n",float_read.value);
C语言strcmp函数
/*************函数原型***************/
int strcmp(char *str1,char * str2);
函数strcmp的功能是 比较两个字符串的大小
。也就是把字符串str1和str2从首字符开始逐个字符的进行比较,直到某个字符不相同或者其中一个字符串比较完毕才停止比较。字符的比较为ASCII码的比较。
若 字符串1大于字符串2
,返回结果大于零;若 字符串1小于字符串2
,返回结果小于零; 若字符串1等于字符串2
,返回结果等于零
C语言sscanf函数
sscanf
通常被用来 解析并转换字符串
,其格式定义灵活多变,可以实现很强大的字符串解析功能
/****************函数原型******************/
#include <stdio.h>
int sscanf(const char *str, const char *format, ...);
str
:待解析的字符串
format
:字符串格式描述
...
:其后是一序列数目不定的指针参数,存储解析后的数据
返回值
:是int类型,即转换成功的数量
普通用法
例如:
int year, month, day;
int converted = sscanf("20191103", "%04d%02d%02d", &year, &month, &day);
printf("converted=%d, year=%d, month=%d, day=%d/n", converted, year, month, day);
输出结果:
converted=3, year=2019, month=11, day=03
%
表示格式转换的开始
高级用法
例如:
char str[32] = "";
sscanf("123456abcdedf", "%31[0-9]", str);
printf("str=%s/n", str);
输出结果:
str=123456
[0-9]
表示这是一个仅包含0-9这几个字符的字符串,前面使用数字 31
修饰词表示这个字符串缓冲区的最大长度(这也是sscanf最为人诟病的地方,很容易出现缓冲区溢出错误,实际上sscanf是可以避免出现缓冲区溢出的,只要在书写任何字符串解析的格式时, 注意加上其缓冲区尺寸的限制
)。
// 表示包含0-9和a-z
sscanf("123456abcdedf", "%31[0-9a-z]", str);
// ^ 表示相反的意思,即不包含a-z 取遇到任意小写字母为止的字符串这里取到6就停止了
sscanf("123456abcdedf", "%31[^a-z]", str);
// * 表示忽略,同时也不需要为它准备空间存放解析结果 即忽略0-9,包含a-z
int ret = sscanf("123456abcdedf", "%*[0-9]%31[a-z]", str);
C语言快速初始化二维数组为一个值(适合大数组初始化)
# define ARR_LEN 100
int arr4[ARR_LEN][ARR_LEN] = { [0 ... (ARR_LEN-1)][0 ... (ARR_LEN-1)] = 10 }; /* 100*100个元素都初始化为10 */
适用于c,可能不适用于cpp
C语言strcpy函数
头文件
:string.h
【只适用于字符串】
- 把源字符数组中的字符串复制到目的字符数组中,字符串结束标志“\0”也一同复制
- 参数1:目的地数组(
char*
) 参数2:源头字符串不会被修改,所以我们用const修饰,比较安全(const char*
) - 返回值:目的地字符串首元素的地址(
char*类型
)
注意:目的地数组必须是可变的
(即不能是const修饰)并且目的地空间的字符串不能是常量字符串
,常量字符串不可被修改
输出常用格式
输出常用格式
// 常用格式化类型:
%d 有符号十进制整数
%u 无符号十进制整数
%x 无符号十六进制数
%o 无符号八进制数
%e e指数科学计数法
%E E指数科学计数法
%s 字符串
%c 字符
%f 浮点数,默认显示6位小数
%.nf 浮点数,精确小数位数为n
%# x 无符号十六进制数加前缀0x
%# o 无符号八进制数加前缀0
%% 百分号
%5d 十进制整数,长度为5(右对齐,空格填充)
%05d 十进制整数,长度为5(右对齐,0填充)
%-5d 十进制整数,长度为5(左对齐,空格填充)
%+5d 十进制整数,长度为5(右对齐,空格填充,显示正负符号)
C语言sprintf函数
头文件
:stdio.h
- 功能跟printf差不多,sprintf函数打印到字符串中(要注意字符串的长度要足够容纳打印的内容,否则会出现内存溢出),而printf函数打印输出到屏幕上
- 参数1:字符数组名(char *) 参数2:格式化字符串(%s,%d,%f…) 参数3(可选):需要输出到格式的变量/常量等等
- 返回值:字符串的长度(相当于strlen,不包括’\0’)(int类型)
//按照某种规则连接成一个字符串时可以用下面方法
/*从理论上讲,他应该比strcat 效率高,因为strcat 每次调用都需要先找到
最后的那个字符串结束字符’\0的位置,而这个给出的例子中,我们每次都利用
sprintf 返回值把这个位置直接记下来了
*/
void main(void)
{
char buffer[200], s[] = "computer", c = 'l';
int i = 35, j;
float fp = 1.7320534f; //
j = sprintf( buffer, " String: %s\n", s ); //
j += sprintf( buffer + j, " Character: %c\n", c ); //
j += sprintf( buffer + j, " Integer: %d\n", i ); //
j += sprintf( buffer + j, " Real: %f\n", fp );//
printf( "Output:\n%s\ncharacter count = %d\n", buffer, j );
}
常见硬件电路原理图缩写
- EN:Enable,使能。使芯片能够工作。要用的时候,就打开EN脚,不用的时候就关闭。有些芯片是高使能,有些是低使能,要看规格书才知道
- CS:Chip Select,片选。芯片的选择。通常用于发数据的时候选择哪个芯片接收。例如一根SPI总线可以挂载多个设备,DDR总线上也会挂载多颗DDR内存芯片,此时就需要CS来控制把数据发给哪个设备
- RST:Reset,重启。有些时候简称为R或者全称RESET。也有些时候标注RST_N,表示Reset信号是拉低生效
- INT:Interrupt,中断。有的INT0,INT1…表示中断0,中断1…以此类推
- CLK:Clock,时钟。时钟线容易干扰别人也容易被别人干扰,Layout的时候需要保护好。对于数字传输总线的时钟,一般都标称为xxx_xCLK,如SPI_CLK、SDIO_CLK、I2S_MCLK(Main Clock)等。对于系统时钟,往往会用标注频率。如SYS_26M、32K等。标了数字而不标CLK三个字,也是无所谓的,因为只有时钟才会这么标
- CTRL:control,控制。写CONTROL太长了,所以都简写为CTRL,或者有时候用CMD(Command)
- D/DATA:数据。I2C上叫做SDA(Serial DATA),SPI上叫做SPI_DI、SPI_DO(Data In,Data Out),DDR数据线上叫做D0,D1,D32等
- A/Address:地址线。用法同数据线。主要用在DDR等地址和数据分开的传输接口上。其他的接口,慢的像I2C、SPI,快的像MIPI、RJ45等,都是地址和数据放在一组线上传输的,就没有地址线了
- TX/RX:Transmit,Receive。发送和接收。这个概念用在串口(UART)上是最多的,一根线负责发送,一根线负责接收。这里要特别注意,一台设备的发送,对应另一台设备就是接收,TX要接到RX上去。如果TX接TX,两个都发送,就收不到数据了
- P(GPIO):很多小芯片,例如单片机,接口通用化比较高,大部分都是GPIO口,做什么用都行,就不在管脚上标那么清楚了,直接用P1,P2,P1_3这样的方式来标明。P多少就是第多少个GPIO。P1_3就是第1组的第3个GPIO。(不同组的GPIO可能电压域不一样)
- EXTI: External Interrupts 外部中断(32单片机)
结构体/枚举/联合体
联合体(共用体)常用于读取浮点数,因为联合体的变量共用同一段内存,改变任意一个变量则另一个变量也会改变,而且内存的长度取决于联合体里成员的最长长度
常用于拆分一个整型或者合并一个整型,还有转换浮点数
# include<stdio.h>
# include<string.h>
//普通声明
struct student {
char name[17]; //char类型 大小:17
int age; //int类型 //大小:4
long phone_number; //long类型 //大小:4
};
//声明结构体类型的同时又用它定义了结构体变量,结构体指针(别名)
typedef struct teacher {
char name[20];
int age;
float kd;
}tea,*ptea;
//枚举
typedef enum{
Monday, //默认是0开始递增
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
sum //数组大小
}Day;
//联合体
union date
{
char a;
char b;
char c;
};
typedef union
{
float num;
char buff[4];
}PP;
void point(ptea p)
{
printf("%s,%d,%.2lf\n",p->name,p->age,p->kd);
}
int main()
{
struct student stu1={"小明",18,6440567}; //初始化赋值
printf("%s,%d,%ld\n",stu1.name,stu1.age,stu1.phone_number);
tea man1;
ptea pman1=&man1;
//man1.name="大佬"; //字符串不能直接赋值,需要用strcpy
strcpy(man1.name,"大佬");
man1.age=45;
man1.kd=99;
printf("%s,%d,%.2lf\n",man1.name,man1.age,man1.kd);
printf("%s,%d,%.2f\n",pman1->name,pman1->age,pman1->kd);
point(pman1); //传结构体指针
point(&man1); //传结构体地址,不能没有&
// 内存对齐问题
/*
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,
结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
*/
printf("stu1内存:%d\n",sizeof(stu1)); //28
//枚举用法
/*
枚举作用
枚举最大值是0XFFFFFFFF,即4个字节
枚举里面的值不一定要从小到大,默认是递增,也可以自己设置值
enum是一组同类型数据的集合,在项目比较大的情况下,用枚举来封装数据能更好的实现模块化。
1.为固定的值命名,当作数组访问的下标,当数组很大时,
比如有几十上百个,那么如果你0-100去表示就很难记住每一个值代表什么意思。
2.通过枚举总值来灵活分配数组的大小,方便从大数组里调取需要的数据
3.把列举的固定值定义为某一种数据类型,这样定义的目的是方便提高代码的可读性和专业性
*/
//1.直接定义枚举值,然后给普通变量赋值
unsigned char day;
day = Tuesday;
printf("day:%d\n",day); //1
//2.定义一个带名称的枚举
//enum day2 = Monday; //需要把别名形式去掉
//3.定义枚举别名
Day day3=Monday;
printf("day3:%d\n",day3); //0
printf("day3内存:%d\n",sizeof(day3)); //枚举一般是占4字节
printf("Sunday:%d\n",Sunday); //6
char arr[sum]={'1','2','3','4','5','6','7'};
printf("arr:%c\n",arr[Thursday]); //4
//联合体(共用体)
/*
1.结构体和联合体的区别在于:
结构体的各个成员会占用不同的内存,互相之间没有影响;
而联合体的所有成员占用同一段内存,修改一个成员会影响其余所有成员
2.联合体占用的内存等于最长的成员占用的内存。联合体中如果对新的成员赋值,就会把原来成员的值覆盖掉
3.联合体一般都是和结构体一起使用的
4.解决浮点型float的读取问题,float是占用4个字节的,
如果将从串口接收的4个字节转换成float呢?联合体就可以可以解决这个问题
*/
union date d1;
d1.a=5;
printf("a:%d,b:%d,c:%d\n",d1.a,d1.b,d1.c); //5 5 5
//浮点数231.5的16进制表示为0x43678000
PP p1;
p1.buff[0]=0x00;
p1.buff[1]=0x80;
p1.buff[2]=0x67;
p1.buff[3]=0x43;
printf("num:%.1lf\n",p1.num); //231.5
return 0;
}
for循环执行顺序
//for循环的基本表达式为
for(表达式1;表达式2;表达式3){
表达式4;
}
执行顺序为:
首次执行时,首先执行表达式1,然后判断表达式2是否成立,不成立则停止执行。表达式2成立的话,再执行表达式4,最后执行表达式3。
之后的循环,首先执行表达式2,判断表达式2是否成立,不成立则停止执行;成立的话,继续执行表达式4,再执行表达式3,直到不满足表达式2,退出循环。
总结
:执行的循环: 1243 243 243 …直到条件2为假则退出循环
extern用法
- extern可以用来在其他模块中公用变量和函数。其用法如:
例如:在a.c文件中定义一个变量 unsigned int intA; intA = 0x00;
在b.c中要操作这个变量,就在 b.c文件中定义 extern unsigned int intA; intA = 0x03;
在b.c中就把intA的值改为了0x03;
然后在a.c文件中查看intA的值,值就为0x03。 - 使用比较方便的方法如下:
1.建立一个v.h 文件,内容为 extern int intM;
2.在任何需要使用的的.c文件中使用,
# include "v.h"
...
intM = 15;
...
就可以直接使用变量intM了。
字符串个数
//用sizeof(arr)获取字符串元素个数
unsigned char arr[]="12345";
printf("%d\n",sizeof(arr)); //输出6,包括末尾'\0'
指针学习(复习)
指针的概念与指针变量的声明
变量的地址
内存单元相当于大楼,比如从0x00、0x01、0x02 一直到 0xNN,我们同样可以说这些编号就是内存单元的地址,基本的内存单元是 字节
(Byte),STC89C52 单片机共有 512
字节的 RAM,就是我们所谓的内存,但它分为 内部 256 字节
和 外部 256 字节
,以内部的 256 字节为例,很明显其地址的编号从 0 开始就是 0x00~0xFF
,C语言定义的各种变量就存在 0x00~0xFF 的地址范围内,而不同类型的变量会占用不同数量的内存单元(即字节)
假如现在定义了 unsigned char a = 1; unsigned char b = 2; unsigned int c = 3; unsigned long d = 4; 这样 4个变量,我们把这 4个变量分别放到内存中,是这样的:a 和 b 都占一个字节,c 占了 2 个字节,而 d 占了 4 个字节,那么,a 的地址就是 0x00,b 的地址就是 0x01,c 的地址就是 0x02,d 的地址就是 0x04,它们的地址的表达方式可以写成:&a
,&b
,&c
,&d
这样就代表了相应变量的地址。
问:变量 c 是 unsigned int 类型的,占了 2 个字节,存储在了 0x02 和 0x03 这两个内存地址上,那么 0x02 是它的低字节还是高字节呢?
这个问题由所用的 C 编译器与单片机架构共同决定,单片机类型不同就有可能不同。比如:在我们使用的 Keil+51 单片机的环境下,0x02 存的是高字节,0x03 存的是低字节
。这是编译底层实现上的细节问题,并不影响上层的应用,如下这两种情况在应用上丝毫不受这个细节的影响:强制类型转换——b = (unsigned char) c,那么 b 的值一定是 c 的低字节;取地址——&c,则得到的一定是 0x02,这都是 C 语言本身所决定的规则,不因单片机编译器的不同而有所改变
要访问一个变量,同样有两种方式:一种是 通过变量名来访问
,另一种自然就是通过 变量的地址
来访问了;在 C 语言中,地址就等同于指针,变量的地址就是变量的指针
,地址输入框输入谁的地址,指向的就是这个人的信息,而给指针变量输入哪个普通变量的地址,它自然就指向了这个变量的内容,通常的说法就是 指针指向了该变量
指针变量的声明
变量的地址往往都是编译系统自动分配的,用户是不知道某个变量的具体地址的,所以我们定义一个指针变量 p,把普通变量 a 的地址直接送给指针变量 p 就是 p = &a;这样的写法
# include<stdio.h>
int main()
{
/*
对于指针变量 p 的定义和初始化,一般有两种方式
*/
//方法 1:定义时直接进行初始化赋值
unsigned char a;
//有个*表示它是专门用来存放变量地址
//unsigned char 这里表示的是这个指针指向的变量类型是 unsigned char 型的
unsigned char *p1 = &a;
//方法 2:定义后再进行赋值
unsigned char b=1;
unsigned char c;
unsigned char *p2;
p2 = &b;
c = *p2; //取地址的数据
printf("%d\n",b); //输出1
printf("%d\n",c); //输出1
//第一个重要区别:指针变量 p 和普通变量 a 的区别
/*
(1)可以写成 p = &a,也可以写成 p = &b,
但就是不能写成 p = 1 或者 p = 2 或者 p = a
(2)指针变量,不可以给它赋值普通的值或者变量,
后边我们会直接把指针变量称之为指针
*/
//第二个重要区别:定义指针变量*p 和取值运算*p 的区别
/*
“*”这个符号,在我们的 C 语言有三个用法
(1)第一个用法很简单,乘法操作就是用这个符号
(2)定义指针变量的时候用,这个地方使用“*”
代表的意思是 p 是一个指针变量,而非普通的变量
(3)还有第三种用法,就是取值运算
*/
return 0;
}
指向数组元素的指针
指向数组元素的指针和运算法则
# include<stdio.h>
int main()
{
unsigned char arr[]={1,2,3,4,5,6,7,8,9};
unsigned char *p;
p = &arr[0];
printf("p=%d\n",*p); //输出1
/*
指针本身,也可以进行几种简单的运算,
这几种运算对于数组元素的指针来说应用最多
*/
//(1)比较运算
/*
1.比较的前提是两个指针指向同种类型的对象,比如两个指针变量 p 和 q
它们指向了具有同种数据类型的数组
2.那它们可以进行<,>,>=,<=,==等关系运算。如
果 p==q 为真的话,表示这两个指针指向的是同一个元素
*/
//(2)指针和整数可以直接进行加减运算
p = p + 1;
printf("p=%d\n",*p); //输出2
//(3)两个指针变量在一定条件下可以进行减法运算
unsigned char *p2;
p2 = &arr[8];
printf("p2-p=%d\n",p2-p); //输出7,这个7代表的是元素的个数,而不是真正的地址差值
/*
在数组元素指针这里还有一种情况,就是数组名字其实就代表了数组元素的首地址
*/
p = &arr[0];
p2 = arr;
printf("p=%d p2=%d\n",*p,*p2); //它们是等价的,输出1 1
/*
指向数组元素的指针也可以表示成数组的形式,也就是说,
允许指针变量带下标,即 p[i]和*(p+i)是等价的(但是不推荐这样写)
*/
//二维数组元素的指针和一维数组类似,加减运算和一维数组也是类似的
unsigned char brr[2][2]={{1,2,3,4},{5,6,7,8}};
unsigned char *p3;
p3 = &brr[0][0];
printf("p3=%d\n",*p3); //输出1
//brr[1]可以看做brr[1][0]
p3 = brr[1];
printf("p3=%d\n",*p3); //输出5
return 0;
}
字符数组和字符指针
const 跟 code 功能是一样的,区别是单片机用const会保存到RAM里,用code会保存到FLASH里
//常量和符号常量
char a = 1; //整型
float b = 3.14; //字符型
char c = 'a'; //字符类型
char s = "abc"; //字符串类型
/*
字符型常量是由一对单引号括起来的单个字符。它分为两种形式,
一种是普通字符,一种是转义字符
(1)普通字符就是那些我们可以直接书写直接看到的有形的字符
它们都是 ASCII 码表中的字符,占1字节
(2)还有一些特殊字符,它们一些是无形的,像回车符、换行符这
些都是看不到的
(3)字符串最后还有一个字符‘\0’这个‘\0’是隐藏的,所以“a”就比‘a’多了一个 ‘\0’,
“a”的就占了 2 个字节,而 ‘a’只占一个字节
(4)字符串中的空格,也是一个字符
*/
ASIIC码
数据类型转换
当不同数据类型之间混合运算的时候,不同类型的数据首先会转换为同一类型,转换的主要原则是:短字节的数据向长字节数据转换
//补充1
比如:unsigned char a; unsigned int b; unsigned int c; a*b=c
当 a=100,b=700,那 c 不是70000而是(70000 - 65536) = 4464,因为int的范围是0~65535
要想得到正确的结果,则需要把c定义成 unsigned long类型,且a或b也需要强制类型转换为 unsigned long类型,即:c = (unsigned long)a * b
//补充2
长字节类型给短字节类型赋值时,会从长字节类型的低位开始截取刚好等于短字节类型长度的位,然后赋给短字节类型
//补充3
有一种特殊情况,就是 bit 类型的变量,比如 bit a=0; unsigned char b; a=(bit)b;这个地方要特别
注意,使用 bit 做强制类型转换,不是取 b 的最低位,而是它会判断 b 这个变量是 0 还是 非0 的值,如果 b 是 0,那么 a 的结果就是 0,如果 b 是任意 非0 的其它值,那么 a 的结果都是 1
逻辑技巧
●把变量的某位清零a &= ~(1<<要清零的位);
注意:位从右往左数,且从O开始数。
●把变量的某几个连续位清零
若把a中的二进制位分成2个一组
即bit0,bitl 为第О组;bit2,bit3为第1组;bit4,bit5为第2组;bit6,bit7 为第3组;
例如要对第1组的 bit2,bit3清零:a &= ~(3<<2*1);
例如对第2组 bit4,bit5清零:a &= ~(3<<2*2);
●对变量的某几位进行赋值对于上述清零完后要进行赋值
若a = 1000 0011 b,此时对清零后的第2组bit4,bit5设置成二进制数 "01 b " 即:a |= (1<<2*2);
变成:a = 10010011 b ,成功设置了第2组的值,其它组不变
1、想让2进制变量中某位的数为1,就让其按位和1`|`
2、想让2进制变量中某位的数为0,就让其按位和0`&`
3、2进制数右移1位就是原数的1半,10进制右移1位就是原数的1/10,16进制右移1位就是原数的1/16
4、2进制数左移1位就是原数的1倍,10进制左移1位就是原数的10位,16进制左移1位就是原数的16倍
左移右移
算数移位
:区分符号的移位 {C语言中直接是定义char m = 3}
逻辑移位
:不区分符号的移位 {C语言中用unsigned char m = 3}
左移时总是移位和补零
;右补0
右移时无符号数
是移位和补零,此时称为逻辑右移
;左补0
而有符号数
大多数情况下是移位和补最左边的位(也就是补最高有效位),移几位就补几位,此时称为算术右移
负数转二进制
对非负数的二进制进行取反、然后+1,便可得其负数的二进制表示
//例
十进制:3
二进制:0000 0011
取反后:1111 1100
加一后:1111 1101
数组元素计算
sizeof(arry) / sizeof(arry[0])
bit 变量类型
51 单片机有一种特殊的变量类型就是 bit
型,bit 型是 1 位数据,只占用 1
个位(bit)的内存,它的优点就是 节省内存空间
,8 个bit 型变量才相当于 1 个 char 型变量所占用的空间
。虽然它只有 0
和 1
两个值,但也已经可以表示很多东西了
优先级表
可参考优先级表
数据类型
在MDK里:
u8(一个字节) — unsigned char (0~255)
u16(二个字节) — unsigned short (0~65535)
u32(四个字节) — unsigned int (0~4294967295)
这里有一个编程宗旨,就是 能用小不用大
。就是说定义能用 1 个字节 char 解决问题的,就不定义成 int,一方面节省 RAM 空间可以让其他变量或者中间运算过程使用,另外一方面,占空间小程序运算速度也快一些。
code 关键字用法
unsigned char 或者 unsigned int 这两个关键字,这样定义的变量都是放在我们的单片机的 RAM中,我们在程序中可以随意去改变这些变量的值。但是还有一种数据,我们在程序中要使用,但是却不会改变它的值,定义这种数据时可以加一个 code 关键字修饰一下,这个数据就会存储到我们的程序空间 Flash 中,这样可以大大节省单片机的 RAM 的使用量,毕竟我们的单片机 RAM 空间比较小,而程序空间则大的多。那么现在要使用的数码管真值表,我们只会使用它们的值,而不需要改变它们,就可以用 code 关键字把它放入 Flash 中了
二维数组
数据类型 数组名[数组长度 1][数组长度 2];
与一维数组类似,数据类型是全体元素的数据类型,数组名是标识符,数组长度 1
和 数组长度 2
分别代表数组具有的 行数
和 列数
。数组元素的下标一律从 0
开始,二维数组的数组元素总个数是两个长度的乘积
二维数组在内存中存储的时候,采用行优先
的方式来存储,即在内存中先存放第 0 行的元素,再存放第一行的元素…,同一行中再按照列顺序存放
,数组元素的数量可以小于数组元素个数,没有赋值的会自动给 0
;此外,二维数组初始化的时候,行数可以省略
,编译系统会自动根据列数计算出行数,但是列数不能省略