C语言基础语法+STM32实践学习笔记 | 指针/寄存器核心应用

26 阅读11分钟

C语言基础语法+STM32实践学习笔记 | 指针/寄存器核心应用

学习说明

  1. 课程来源:B站视频:【颠覆传统学习法】韦东山带你用STM32玩转C语言:内存指针+嵌入式双技能速成
  2. 学习目标:掌握C语言底层语法在STM32硬件实操中的应用;
  3. 适用人群:嵌入式入门、想巩固C语言底层用法的学习者;

[toc]


1、零碎知识点

  • volatile:中文意思是易变的 (面试笔试常见问题)

    • volatile int a;明确的告诉编译器,你不要来优化我;
  • volatile signed char c;表示有符号位的变量。

  • 大/小 字节序 (面试笔试常见问题)

    0x12345678 (高位 ==> 低位)

    • 字节序存储方式
      小字节序(Little-endian)低位数据存在低地址,主流 CPU 的默认选择。
      大字节序(Big-endian)低位数据存在高地址。
    • 网络通信:TCP/IP 协议规定使用大端序作为网络字节序,保证不同架构设备之间的数据传输一致性(嵌入式设备 <=> x86 )

    • 部分嵌入式 CPU:如传统 PowerPC、SPARC,以及 ARM 架构的大端模式配置(ARM 支持双端序,可通过寄存器配置)

  • typedef 使用

typedef int TNT_TYPT; //以后TNT_TYPE,和int一样,可以定义变量;

2、指针/地址(指针就是地址

2.1 指针基础

  • cpu指针大小
    32位(默认)4字节 (4*8 = 32)
    64位置8字节 (8*8 = 64)
  • char c = 'A';
    char* addrc = &c; //char类型的指针 可以存入char类型变量的地址;  
    // char类型的指针addrc <==> char类型变量c 
    // 如果不知道变量类型,可强制转换类型: int* c = (int*)0x20000000;
    *addrc = 'B'; //往该指针 指向地址 中的数值写入'B'    //使用指针访问变量c这块内存;为什么可以?因为addrc等于c的地址
    
    //int* p   //p is point to int 
    
    // 指针大小
    char a = 1;
    int b = 2;
    char* addra = &a;
    int* addrb = &b;
    
    printf("sizeof(*addra) = %d\r\n",sizeof(*addra));  // == 1
    printf("sizeof(*addrb) = %d\r\n",sizeof(*addrb));    // == 4
    
    printf("sizeof(char*) = %d\r\n",sizeof(char*));  // == 4
    printf("sizeof(int*) = %d\r\n",sizeof(int*));    // == 4
    

2.2 使用指针访问硬件

int a = 1;
int* p = &a; //int类型的指针 可以存入int类型变量的地址; 
*p = 2; //使用指针访问变量c这块内存;为什么可以?因为addrc等于c的地址
==》
p = 其他硬件的地址;
*p = val;/* 写硬件的时候要考虑寄存器 所占位数 和 *p指针类型 所对应的位数;若寄存器有16位,但是定义的是 char* p (一个字节,8个位),则只能写该寄存器的低八位; 后续只能通过修改指针 p 的类型 或者 将p指向的地址+1;访问高 8 位*/
v = *p; //读硬件
// 最简单的 用C语言访问硬件的代码
// 在实际使用过程中,需要翻阅芯片手册 来获取对应寄存器的地址;写过程中用位操作,来修改对应的位;
int mymain(void)
{
    //指针类型尽量匹配寄存器位数;
    volatile int* p = (volatile int*)0x20000000;//0x20000000 是地址
    *p = 0x12345678;							//写0x20000000 地址中写入 0x12345678;
    p = (volatile int *)0x40021414; /* GPIO_ODR寄存器的地址 */

    *p = *p & 0xFFFFFDFF; /*灯亮  使得第9位为0*/   // *p = *p & ~(1<<9)
    *p = *p | 0x00000200; /*灯灭  使得第9位为1*/
    //*p = (1<<9);
    return 0;    
}

//课后作业:修改指针p的类型
int mymain(void)
{
	/*
	volatile int* p = (volatile int*)0x20000000;
	*p = 0x12345678;
	p = (volatile int *)0x40021414; //GPIO_ODR寄存器的地址 
	*p = 0;			//灯亮
	*p = 0x200;  //灯灭
	*/
    volatile char* p = (volatile char*)0x20000000;
    *p = 0x00;
    p = (volatile char *)0x40021415; /* GPIO_ODR寄存器的地址 */
    //*p = *p & 0xFD;			/*灯亮*/
    *p = *p | 0x02;  /* 灯灭 */
    return 0;    
}

3、数组

volatile char a[] = "1234567890";//10个字符
printf("sizeof(a) = %d\r\n",sizeof(a));  //11字节,结尾有个'\0'

volatile char a[] = {'a','b','c'};// ==> 输出可能为 abc+乱码
volatile char a[] = {'a','b','c',0};// ==>abc 无乱码

3.1 数组名就是有个指针

int arr[] = {1, 2, 3, 4, 5};
int *p = arr; // 等价于 int *p = &arr[0];

// 以下两种表达式等价
printf("%d\n", arr[1]);   // 输出 2
printf("%d\n", *(p + 1)); // 输出 2   //p[1] 等价于 *(p+1)
  • arr是常量指针,“arr++”是错误的

3.2 多维数组

int arr[2][3] = { {1, 2, 3}, 
				  {4, 5, 6} };

printf("%d\n", arr[0][1]); // 输出 2
printf("%d\n", arr[1][2]); // 输出 6

int (*p)[3] = arr; // 指向一个包含 3 个整数的数组的指针
printf("%d\n", p[0][1]); // 输出 2
  • 指针数组 是有个数组,其中每个元素都是指针;
char *strs[] = {"Hello", "World", "C Programming"};  
printf("%s\n", strs[0]); // 输出 Hello

3.3 问题:

下面语句有什么差别:

char str[] = "abc"; 
char *p = "abc";

//是否可以执行
p[1] = 'A';
  • 不可以,
  • char *p = "abc";这是字符指针指向字符串常量,p 是指针变量(存储地址),存储在栈区;而 "abc" 是字符串常量,存储在只读数据区(.rodata)。p 仅保存了字符串常量 "abc" 的首地址,字符串本身不可修改(修改会触发段错误)。

4、结构体

  • 结构体 字节对齐;目的是为了cpu执行效率; (面试笔试常见问题)

4.1 结构体指针

struct Student {
    char name[20];
    int age;
    char sex;
    int score;
};

struct Student a;
struct Student* p;   //指向结构体的 指针
p = &a;

//写入数据的 的区别
a.age = 18;
p->age = 24; //指针 指向 

4.2 结构体指针访问硬件


#include <stdio.h>
#include <string.h>
//将寄存器及其偏移地址,写成结构体的形式,并且使用结构体指针来访问硬件寄存器
struct GPIORegs {
    volatile unsigned int moder;
    volatile unsigned int otype;
    volatile unsigned int speedr;
    volatile unsigned int pupdr;
    volatile unsigned int idr;
    volatile unsigned int odr;//控制LED亮灭
    volatile unsigned int bsrr;
    volatile unsigned int lckr;
    volatile unsigned int afrl;
    volatile unsigned int afrh;
};

int mymain(void)
{
    //struct GPIORegs f;  // f是内存里一块区域, f.odr不会访问到GPIOF的寄存器
    //需要使用指针来实现对寄存器的访问
    struct GPIORegs *p = (struct GPIORegs *)0x40021400;  /* GPIOF的基地址 */
    
    p->odr |= (3<<9);  /* 熄灭LED1,LED2 */   //p->odr = p->odr | (3<<9)
    p->odr &= ~(1<<9);  /* 点亮LED1 */
    p->odr |= (1<<9);  /* 熄灭LED1 */
    
    return 0;    
}

5、函数

5.1 参数传递与栈

  • 当程序运行到mymain函数时,就会给它分配空间,sp寄存器就会往低地址移动,给mymain函数分配栈空间,mymian函数中的局部变量就存在该栈空间中;在函数1中运行到函数2时,sp寄存器会继续往低地址移动,开辟函数2的栈空间;两者不相交;
  • 递归就是在函数1中调用函数1,同样会开辟多个函数1的栈空间;
void swap(int *a, int *b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}
int main()
{
	int x = 3, y = 5;
	swap(&x, &y); // x和y的值会被交换  传递的是地址
	return 0;
}
  • 函数间参数的传递传递的是值;
    • 当传递的是地址(形参是指针)时,函数可通过该地址可以访问到该变量;
    • 当传递的不是地址(形参也不是指针),这该函数无法访问到之前的变量;
  • 当函数结束后,sp会返回到之前的值,将执行完毕的函数栈空间回收;

5.2 递归函数

  • 少用,对栈空间消耗太大;
int factorial(int n) //计算 阶乘
{
    if (n == 0) 
    {
    	return 1;
    }
    return n * factorial(n - 1);
}
5.2.1 递归函数_栈使用情况
  • 不断开辟函数栈空间

1_递归函数_栈使用情况.png

5.2.2 递归的注意事项
  • 必须有一个基例(停止条件),否则会导致无限递归。
  • 递归可能占用更多栈空间,导致栈溢出。

5.3 作用域

在C语言中,作用域(Scope)决定了变量、函数等标识符在程序中的可见性生命周期。作用域分为 以下几类:

  • 全局变量 :在函数外部定义的变量,作用域为整个程序。
  • 局部变量 :在函数内部代码块内定义的变量,作用域仅限于定义它们的函数或代码块。
5.3.1 全局变量
  • 全局变量可以通过外部定义( extern )在其他文件中使用。
5.3.2 局部变量
  • 局部变量在函数或代码块执行时创建,函数或代码块执行完毕后销毁;
5.3.3 嵌套在代码块中的局部变量
for(int i=0; i<10; i++) //其中i就是代码块中的局部变量,仅在for循环中生效;
5.3.4 静态变量(static)
  • 若在函数内定义:
    • 静态的局部变量,其作用域仅限函数内部;
    • 仅分配一次,仅初始化一次;
    • 该变量的内存并不在该函数的栈里面;局部静态变量和全局变量均不放在栈里面,而是放在数据段
  • 若在函数外定义:
    • 静态的全局变量,其作用域为该.c文件;
    • 避免了多个文件中有相同变量名的问题;(当同一文件遇到同名变量,优先使用作用域小的变量)

5.4 头文件

  • 头文件(以.h 为扩展名)用于声明函数定义宏类型等。它的主要作用是将程序的接口与实现分离,使代码模块化;
5.4.1 常见头文件用途
  • 将函数的声明放在头文件中,这样多个源文件可以共享这些函数的定义,而无需重复编写声明
5.4.2 头文件包含
#include <stdio.h>
#include "math.c"    //当前目录下
5.4.3 防止重复包含
//a.h
#ifndef _A_H
#define _A_H

//头文件内容

#endif

/*
#if 0
#endif
*/
5.4.4 如何创建自己的include文件?
  • 建立自己的myinclude文件

2_myinclude.png

  • 添加include path

3_包含新的include.png

5.5 多文件编程

  • 多文件编程步骤:

    • 创建.c、.h文件,比如:person.cperson.h
    • 把.c文件加入工程,比如:把person.c加入工程,person.h无需加入工程
    • 在其他文件包含person.h,调用person.c里的函数
  • 优势:便于模块化大型项目开发;便于团队协作;方便复用;

5.6 函数指针

  • 函数指针是存储函数内存地址变量,可以通过该指针间接调用函数。与数据指针不同,函数指针指向 的是可执行代码段 (函数实现的地址)。
5.6.1 声明语法

return_type (*pointer_name)(parameter_types);

//示例:定义函数指针
int (*pFunc)(int,int);//指向返回int,接受两个int参数的函数
//示例:定义函数指针类型
typedef int (*pFunc)(int, int);  // pFunc是"函数指针类型"
pFunc p1, p2;  // 定义了p1, p2,它们是"函数指针"变量
  • typedef
int  a;  // a是变量
int  *a; // a是指针变量
typedef  int a;  // a是"int", 是类型
typedef  int* a; // a是"int *", 是类型
int add(int a, int b);  // add是函数
int (*add)(int a, int b);  // add是函数指针, 是一个变量
typedef int (*add)(int a, int b);  // add是"函数指针类型", 是一个类型
5.6.2 简单示例
#include <stdio.h>
int add(int a, int b) 
{
	return a + b;
}
int main() 
{
    int (*pAdd)(int, int);
    pAdd = add;//指向函数的指针,pAdd等于add函数的地址;
    // int (*pAdd)(int, int) = &add;  // 或直接写 pAdd = add;
    
    // 两种调用方式等价
    int result2 = pAdd(3, 5);//跳到pAdd所表示的地址去执行,add的地址
    int result1 = (*pAdd)(3, 5);
    
    printf("Results: %d, %d\n", result1, result2);
    return 0;
}
5.6.3 函数指针作为参数
#include <stdio.h>
void process(int (*operation)(int, int), int x, int y) 
{
    int result = operation(x, y);
    printf("Result: %d\n\r", result);
}
int add(int a, int b)
{
	return a+b;
}
int sub(int a, int b)
{
	return a-b;
}
int main() {
	process(add, 5, 3);      
}
5.6.4 函数指针数组
#include <stdio.h>
void process(int (*operation)(int, int), int x, int y) 
{
	int result = operation(x, y);
	printf("Result: %d\n\r", result);
}

int add(int a, int b)
{
	return a+b;
}
int sub(int a, int b)
{
	return a-b;
}

typedef int (*f_type)(int a, int b);

int main() 
{
    f_type f_arr[2] = {add, sub};
    int v1 = 4, v2 = 2;
    int result;
    for (int i = 0; i < 2; i++)
    {
    	result = f_arr[i](v1, v2);//用f_arr[i]的值作为地址,去执行Flash上的代码;
    }
    printf("f %d, result = %d\n\r", i, result);
    return 0;
}
5.6.5 结构体中的函数指针
struct person
{
	char *name;
	int sex;
	void (*print)(struct person *p);
};

void printName(struct person *p) 
{
	printf("Name: %s\n\r", p->name);
}
void printAll(struct person *p) 
{
	printf("Name: %s\n\r", p->name);
	printf("Sex: %s\n\r", p->sex ? "Male" : "Female");
}

int main() 
{
    struct person per1 = {"zhangsan", 1, printName};
    struct person per2 = {"lili", 0, printAll};
    per1.print(&per1); 
    per2.print(&per2); 
    return 0;
}

注意事项

  • dubug结束后,一定要把断点去掉,再关闭debug;否者会卡死;

  • "我们要引入一个新的知识点是为了解决一个新的问题 !"



欢迎一起交流讨论。