C语言基础语法+STM32实践学习笔记 | 指针/寄存器核心应用
学习说明
- 课程来源:B站视频:【颠覆传统学习法】韦东山带你用STM32玩转C语言:内存指针+嵌入式双技能速成 ;
- 学习目标:掌握C语言底层语法在STM32硬件实操中的应用;
- 适用人群:嵌入式入门、想巩固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 递归函数_栈使用情况
- 不断开辟函数栈空间
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文件
- 添加
include path
5.5 多文件编程
-
多文件编程步骤:
- 创建.c、.h文件,比如:
person.c、person.h - 把.c文件加入工程,比如:把
person.c加入工程,person.h无需加入工程 - 在其他文件包含
person.h,调用person.c里的函数
- 创建.c、.h文件,比如:
-
优势:便于模块化大型项目开发;便于团队协作;方便复用;
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;否者会卡死; -
"我们要引入一个新的知识点是为了解决一个新的问题 !"
欢迎一起交流讨论。