C语言strstr函数

char *strstr(const char *str1, const char *str2)
// 返回值为char * 类型( 返回指向 str1 中第一次出现的 str2 的指针);如果 str2 不是 str1 的一部分,则返回空指针    

C语言strchr函数

char *strchr(const char *str, int c)
功能:在参数 str 所指向的字符串中搜索第一次出现字符 c(一个无符号字符)的位置
参数1:str – 要被检索的 C 字符串
参数2:c – 在 str 中要搜索的字符 
返回值:该函数返回在字符串 str 中第一次出现字符 c 的位置,如果未找到该字符则返回 NULL    
// 示例
#include "stdio.h"
#include "windows.h"

const char str[] = "+CGPADDR: 0,\"100.113.231.185\"";
char ip[16];
int main(void)
{
    const char *start = strchr(str, '"');
    if (start == NULL) {
        printf("双引号未找到\r\n");
        return 1;
    }
    const char *end = strchr(start + 1, '"');
    if (end == NULL) {
        printf("双引号未找到\r\n");
        return 1;
    }
    size_t len = end - start - 1;
    strncpy(ip, start + 1, len);
    ip[len] = '\0';

    printf("%s\r\n", ip);
    system("pause");
    return 0;
}

位域

位域通常在以下情况下使用:

  1. 对内存空间有严格要求:如果你在嵌入式系统或者其他内存受限的环境中工作,使用位域可以有效地节省内存空间。位域允许你对数据进行细粒度的位级编码,从而在不牺牲功能的情况下减少占用的存储空间。
  2. 需要对数据进行位操作:位域适用于需要进行位级操作的场景。例如,你可能需要将一个整型字段拆分成多个单独的位域来表示各种状态、标志或权限。
  3. 数据传输或存储要求:有时候,你可能需要将多个信息打包到一个固定长度的数据结构中进行传输或存储。在这种情况下,位域可以帮助你高效地使用二进制位来表示和解析这些数据。
  4. 确保数据结构与硬件寄存器匹配:在与硬件进行交互的应用程序中,位域可以确保数据结构与硬件寄存器的位布局一致,从而简化与硬件的通信。

缺点

  1. 不可移植,因为字节(byte)中的位(bit)和字(word)中的字节(byte)的放置顺序是取决于编译器的。
  2. 机器的大小端,得出的结果可能在高低位地址实际排放的结果会相反。
  3. 不能使用 & 获取位域成员的地址,因为不能保证位域成员在字节(byte)地址上。
  4. 位字段用于将更多的变量打包到更小的数据空间中,会导致编译器产生额外的代码来操作这些变量。这在代码大小和执行时间方面都会付出代价。(编译器必须进行位分割/错误对齐的访问)

总结就是位域适用于通信协议是以位为最小的那种,比如

bit0:帧头 bit1~bit7:数据

要是不是这种的话用普通的就行了

// 定义包含位域的数据结构
struct data_packet {
    uint16_t sensor_value1: 10;   // 传感器值1,占用10个二进制位
    uint8_t sensor_value2: 6;     // 传感器值2,占用6个二进制位
    uint8_t status: 4;            // 状态字段,占用4个二进制位
    uint8_t reserved: 2;          // 保留字段,占用2个二进制位
};

int main() {
    struct data_packet packet;
    
    // 设置数据包的值
    packet.sensor_value1 = 512;
    packet.sensor_value2 = 20;
    packet.status = 3;
    packet.reserved = 0;

    // 将数据包发送或存储
    // ...
    
    // 从接收到的数据中解析出数据包
    // ...

    // 解析数据包并获取各个字段的值
    uint16_t received_value1 = packet.sensor_value1;
    uint8_t received_value2 = packet.sensor_value2;
    uint8_t received_status = packet.status;
    
    // 使用解析出的值进行相应的处理
    // ...

    while (1) {
        // 主循环
    }
}

结构体字节对齐

结构体字节对齐,pragma pack,attribute(packed)

一般编译器的结构体都会字节对齐,有时候我们不想它对齐就要用到 __attribute__((packed)),是GUN C 的一大特色,是一种机制,他可以设置函数属性,变量属性,类型属性,packed就是个类型属性,通常在跨平台的基于数据结构的网络通信的时候特别要注意字节的对齐方式,以为在不同的系统中,某些类型的大小不一样

stm32里面可以这样定义结构体:

/* 数据头结构体 */
typedef __packed struct
{
    uint32_t head; // 包头
    uint8_t ch;    // 通道
    uint32_t len;  // 包长度
    uint8_t cmd;   // 命令
    //  uint8_t sum;      // 校验和
} packet_head_t;

do…while(0)

参考文章:gitee博主-duapple

用法1—在宏里使用

写在do while里主要是封装成单语句,防止有时用for或者if调用宏时外面没加{},导致运行异常

#define strs_free(ptr)  \
    do{ \
        free(ptr);  \
        ptr = NULL; \
    }while(0)   

int main()
{
    char * arr = (char*)malloc(50);
    for(int i = 0; i < 50; i++)
    {
        *(arr + i) = '1';
    }
    printf("%c--%c\r\n",arr[0],arr[49]);
    strs_free(arr);

    system("pause");        
}

用法2–避免由宏引起的警告

内核中由于不同架构的限制,很多时候会用到空宏,。在编译的时候,这些空宏会给出warning,为了避免这样的warning,我们可以使用do{…}while(0)来定义空宏:

#define EMPTYMICRO do{}while(0)

用法3–避免使用goto控制程序流

在一些函数中,我们可能需要在return语句之前做一些清理工作,比如释放在函数开始处由malloc申请的内存空间,使用goto总是一种简单的方法

int foo()
{
    somestruct *ptr = malloc(...);

    dosomething...;
    if(error)
        goto END;
    dosomething...;
    if(error)
        goto END;
    dosomething...;
END:
    free(ptr);
    return 0;
}

但由于goto不符合软件工程的结构化,而且有可能使得代码难懂,所以很多人都不倡导使用,这个时候我们可以使用do{…}while(0)来做同样的事情:

int foo()
{
    somestruct *ptr = malloc(...);
    do
    {
        dosomething...;
        if(error)
            break;
        dosomething...;
        if(error)
            break;
        dosomething...;
    }
    while(0);

    free(ptr);
    return 0;
}

大小端

这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为 8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如和将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。例如一个16bit的short型x,在内存中的地址为0x0010,x的值为0x1122,那么0x11为高字节,0x22为低字节。对于大端模式,就将0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中。

  • 所谓的大端模式(BE big-endian),是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中(低对高,高对低)
  • 所谓的小端模式(LE little-endian),是指数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中(低对低,高对高)

常见的单片机大小端模式:

(1)KEIL C51中,变量都是大端模式的,而KEIL MDK中,变量是小端模式的

(2)SDCC-C51是小端寻址,AVRGCC 小端寻址

(3)PC小端,大部分ARM是小端

(4)总起来说51单片机一般是大端模式,32单片机一般是小端模式

__inline关键字

在嵌入式单片机里,使用 __inline 关键字修饰函数的话,可以提升速度,以空间换速度,适用于要求速度快,IO控制,频繁被调用的短小函数(一般要求控制在10行内)

但是如果一个函数的代码行数比较多,或者包含循环、递归等控制结构,就不太适合进行内联优化,因为这些操作都会导致代码膨胀,增加缓存命中率的风险

因此,在使用 __inline 进行函数优化时,需要考虑以下几点:

  1. 适当控制使用 __inline 的函数量,避免滥用,应该根据具体情况来选择是否使用 __inline
  2. __inline 优化的函数应该尽可能简短,避免包含复杂的算法或者数据结构操作。
  3. 内联函数的实现需要保证正确性,并进行充分测试,避免因为代码重复导致错误难以排查。
// 举例
static __inline void CRC_Check(void)
{
    .....
}

取模

int minValue = X1;
int maxValue = X2;
int rangeLength = maxValue - minValue + 1;

imageIndex = (imageIndex - minValue + 1) % rangeLength + minValue;


// 实现 2~6之间循环
// 使用这种方法可以对连续或者不连续两个数之间的循环有很大用处,比下面这种简洁:
uint8_t num = 2;
num++;
if(6 == num)
{
    num = 2;
}

// 下面这种方法简洁:
num = (num - 2+1) % 5 + 2;

如果想要倒序的话,比如:9,8,7,6,5,4,3,9,8,7,…可以使用三目

num = (num > 3) ? num - 1 : 9;

const注意

  1. const uint8_t *P 或 uint8_t const *P (等价的)

const修饰的是*P,指针指向的数据只读, 指针本身可以读写

  1. uint8_t* const P

const修饰的是P,指针本身只读,指针指向的数据可以读写

  1. const uint8_t* const Puint8_t const * const P (等价的)

const同时修饰*P与P,指针与指针指向的数据均只读

ASCII和中文区分

在程序里一般可以通过判断编码值是否大于127来区分是不是ASCII,因为ASCII可打印字符的范围是 31~127,中文是采用双字节编码方式,即每个汉字被表示为两个连续的字节,其范围是 0x4E00-0x9FFF,都是大于 127 的。因此,如果一个字符的编码值大于 127,则可以断定它是汉字或其他非 ASCII 字符

注意:这种判断方式不适用于一些特殊情况,比如文本中含有日语、韩语等其他语言的字符

// 比如传入一个字符串指针
void Func(const char *pStr)
{
    if((*pStr) > 127)
    {
        // 是中文
    }
    else
    {
        // 是ASCII
    }
}

断言

此宏定义用于判断传入参数是否合法,在 stm32f1xx_hal_confh

static void LCD_ShowCHN(uint16_t usX, uint16_t usY, const char * pStr, uint16_t usColor_Background, uint16_t usColor_Foreground,CHN_font_t font)
{
    //检查输入参数是否合法
	assert_param(IS_ASCII_font(font));
}

结构体数组

结构体数组是一种存储多个相同结构体类型数据的方式,可以方便地进行批量处理和访问,常用于对数据的组织和管理。

假设我们需要保存多个人的基本信息,包括姓名、年龄和性别。我们可以使用一个 person 结构体来表示每个人的信息

typedef struct {
    char name[10];
    int age;
    char gender;
} person;

person people[3] = {
    {"Alice", 24, 'F'},
    {"Bob", 32, 'M'},
    {"Charlie", 45, 'M'}
};

int main(void)
{
    // 可以通过下标来访问特定的人的信息,例如 people[0].name 表示第一个人的姓名,people[2].age 表示第三个人的年龄
    printf("%s\r\n", people[0].name);
    // 注意修改字符串类型的需要使用复制函数不能直接=赋值,而且注意溢出
    strncpy(people[0].name, "小明撒打111", sizeof(people[0].name) - 1);
    people[0].name[sizeof(people[0].name) - 1] = '\0';
    printf("%s\r\n", people[0].name);
    system("pause");
    return 0;
}

// 输出结果
Alice
小明撒打1

RGB转RGB565

传入一个RGB十六进制返回一个十进制,用于某些TFT显示颜色

unsigned short RGB565(unsigned int rgb16)
{
    // 将 RGB16 进制颜色拆分成 R、G、B 三个通道的数值
    unsigned char red = (rgb16 >> 16) & 0xFF;
    unsigned char green = (rgb16 >> 8) & 0xFF;
    unsigned char blue = rgb16 & 0xFF;

    unsigned short color = 0;
    color |= (red >> 3) << 11;  // 将red的高5位放到color的高5位
    color |= (green >> 2) << 5; // 将green的高6位放到color的中间6位
    color |= blue >> 3;         // 将blue的高5位放到color的低5位

    return color;
}

__IO

IO 是一个 GCC 编译器的扩展关键字,它用于告诉编译器该变量是一个 I/O(输入/输出)变量,即表示该变量在读写时可能会被修改。这个关键字让编译器知道该变量需要特殊对待,以便避免某些不必要的编译器优化导致的错误行为。一般来说,在操作 I/O 硬件寄存器时最好加上 IO 关键字,以确保正确读写 I/O 变量的值

它只会影响编译器的优化策略,而不会对程序的运行时行为产生实质性的影响

字符串含中文报错

文件格式问题,之前遇到过,用 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’

解决方案有两种:

  1. 将keil安装目录的Segger路径,如D:\Keil_v5\ARM\Segger添加到系统环境变量(对我来说没起作用)
  2. 用另一部电脑测试不报错的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(整型数据,存储目标字符串的数组,想要结果的进制)

atof函数:把字符串转化为浮点数(float),但是注意首元素要是正负号或者数字,不然返回0,有相同作用的函数是 strtod

字符转正常数字

有时候要在字符串或者字符数组里提取里面的整型数字,可以直接 -'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~9a~zA~Z 可以直接加个单引号’'来判断不需要判断它的字符编号

浮点数存储规则

参考文章:

联合体在单片机编程中的应用

C语言浮点型在内存中的存储 ----足够通透足够细

任意一个二进制浮点数 V 可以表示成下面的形式:

(1)S×M×2E(-1)^S\times M \times2^E

其中,

(1)s(-1)^s表示 符号位,当s=0s = 0,V为 正数 ;当s=1s = 1,V为 负数

M表示有效数字,1M21 \leq M\leq 2

2E2^E表示 指数位

对于 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

231.5的二进制:1110 0111.1\text{231.5的二进制:1110 0111.1}

是正数,所以S=0\text{是正数,所以S=0}

11100111.1>1.11001111×27故E=7+127=134 转换成二进制:100001101110 0111.1 --> 1.11001111 \times 2^7 \text{故E=7+127=134 转换成二进制:10000110}

M=11.11001111=0.11001111M=1-1.11001111=0.11001111

最终结果:S+E+M= 0 100,0011,0 110,0111,1000,0000,0000,0000 = 0x43678000\text{最终结果:S+E+M= 0 100,0011,0 110,0111,1000,0000,0000,0000 = 0x43678000}

//单片机的应用

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);

实战单片机:

提取浮点数

/*
* @function: Str_To_Float
* @param: p_Str -> 待提取的字符串 save_var -> 提取结果存储地址
* @retval: None
* @brief: 提取字符串里面=后面的浮点数
*/
static inline void Str_To_Float(const char *p_Str, float *save_var)
{
	const char* start = strchr(p_Str, '='); // 查找等号的位置
	
    if (start == NULL)
    {
        return;
    }
    sscanf(start + 1, "%f", save_var);	// 正常提取
    // sscanf(start + 1, "%f[^,]", save_var); // 使用 sscanf 函数提取浮点数值截止字符为,
}

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 型变量所占用的空间。虽然它只有 01 两个值,但也已经可以表示很多东西了

优先级表

可参考优先级表

数据类型

在Keil里通过串口打印得出的结果

在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;此外,二维数组初始化的时候,行数可以省略,编译系统会自动根据列数计算出行数,但是列数不能省略