目录
读者姥爷们的收藏,是对我技术的最大认可,希望得到你的收藏,感谢阅读,感谢支持!!!
前言:本篇教程,仅供学习,编写不易,请勿搬运,感谢理解
嵌入式C/C++语言教程专栏文章连接
你真的理解,volatile关键字嘛?,本篇3K字,放心食用,立志最细。(看不会找我!!!)_单片机防编译优化-优快云博客
绪论
嵌入式开发虽然用的是C语言,但是有些语法经常用,用些语法不常用,或者就用不上,本篇教程旨在为大家带来最为常用的C语言语法与教程,想要成为开发高手这些是需要熟练掌握的,同时本篇教程写下自己的心得,与参考正点原子,一只弱学狗,连接放在最后,希望对你有所帮助!!!
如果你喜欢这篇文章,请留下点赞,数量多会用心为大家出文章和教程的。
typedef关键字
这个关键字在标准库函数定义里面用的多,还有就是自己定义匿名结构体在.h文件里面,主要用法有两种,定义匿名结构体,和定义变量类型,定义指针函数。
定义匿名结构体基本语法示例
typedef struct {
// 成员变量定义
数据类型 成员名;
...
} 类型名;
typedef struct {
int x;
int y;
} Point;
使用typedef关键字定义匿名结构体,这里类型名的作用是,可以通过特定类型名声明定义的该结构体类型。
定义变量类型基本语法示例
typedef 原始类型 新类型名;
typedef int MyInt; // 定义 int 的别名为 MyInt
typedef float Real; // 定义 float 的别名为 Real
使用typedef重新声明定义变量类型,这里需要注意的是原始类型和新类型名字的位置,不要高反,这里重点是相较于#define区别是,typedef是编译器处理,#define是预处理指令处理,重点是后者是简单的文本替换,不具有类型检查。
这里typedef可以用于指针函数定义新名称,指针定义新名称,数组定义新名称,但是常用的也就是,给基本变量类型,定义新名称。
定义函数指针基本语法示例
typedef 定义函数指针也是一种在库函数中较为常用的用法,但是这种用法不同与常规的关键字语法。
#include <stdio.h>
// 定义函数指针类型
typedef void (*IRQHandler)(void); //定义了一个新类型 IRQHandler,它是指向返回类型为 void 且无参数的函数的指针。
// 示例函数
void MyIRQHandler(void) {
printf("Interrupt handler called!\n");
}
int main(void) {
// 声明一个 IRQHandler 类型的变量
IRQHandler handler;
// 将函数指针指向具体的函数
handler = MyIRQHandler;
// 调用函数,通过函数指针
handler();
return 0;
}
库函数相关调用
typedef struct {
uint32_t GPIO_Pin; // 引脚号
uint32_t GPIO_Mode; // 模式
uint32_t GPIO_Speed; // 速度
} GPIO_InitTypeDef;
typedef GPIO_InitTypeDef* GPIO_InitPtr; // 定义结构体指针类型
上面的示例代码里面,定义了匿名结构体还用 typedef 这个关键字声明了一种指向 GPIO_InitTypeDef 类型的指针,下面给一下代码的解析。
首先 GPIO_InitTypeDef 是用来声明GPIO的结构体类型声明关键字,带了*就是指针类型,这里的语法格式 typedef 旧类型 新类型 所以 GPIO_InitPtr 这个类型就是指向 GPIO_InitTypeDef 类型的指针函数,用这个关键字声明 GPIO 结构体类型然后再赋值完全没有问题。
讲的深可能对小白很难,慢慢来吧!!!
keil5特有变量类型
在keil5工程中有一些变量类型是特有的,这些变量类型使用typedef被封装在stdint.h头文件中,是在学习C语言中从来没有讲解过的,提供的这些变量类型对于有些开发者为了自己的开发方便,可能还会使用宏定义进行二次封装。
特有相关库函数示例
下面的代码是stdint.h头文件里面的内容,给大家放到这里了,这面使用了大量的typedef定义变量类型,定义了大量的新的变量类型。
/* 7.18.1.1 */
/* exact-width signed integer types */
typedef signed char int8_t;
typedef signed short int int16_t;
typedef signed int int32_t;
typedef signed __INT64 int64_t;
/* exact-width unsigned integer types */
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
typedef unsigned __INT64 uint64_t;
/* 7.18.1.2 */
/* smallest type of at least n bits */
/* minimum-width signed integer types */
typedef signed char int_least8_t;
typedef signed short int int_least16_t;
typedef signed int int_least32_t;
typedef signed __INT64 int_least64_t;
/* minimum-width unsigned integer types */
typedef unsigned char uint_least8_t;
typedef unsigned short int uint_least16_t;
typedef unsigned int uint_least32_t;
typedef unsigned __INT64 uint_least64_t;
/* 7.18.1.3 */
/* fastest minimum-width signed integer types */
typedef signed int int_fast8_t;
typedef signed int int_fast16_t;
typedef signed int int_fast32_t;
typedef signed __INT64 int_fast64_t;
/* fastest minimum-width unsigned integer types */
typedef unsigned int uint_fast8_t;
typedef unsigned int uint_fast16_t;
typedef unsigned int uint_fast32_t;
typedef unsigned __INT64 uint_fast64_t;
/* 7.18.1.4 integer types capable of holding object pointers */
#if __sizeof_ptr == 8
typedef signed __INT64 intptr_t;
typedef unsigned __INT64 uintptr_t;
#else
typedef signed int intptr_t;
typedef unsigned int uintptr_t;
#endif
/* 7.18.1.5 greatest-width integer types */
typedef signed __LONGLONG intmax_t;
typedef unsigned __LONGLONG uintmax_t;
需要注意的是,int8_t
~ int64_t
:分别表示8、16、32、64位的有符号整数。uint8_t
~ uint64_t
:分别表示8、16、32、64位的无符号整数,这里关于有符号和无符号的定义式,前者可以是负数数据位大小包含负数范围,无符号类型只能是正数类型,不包含负数类型。
特有变量类型二次定义示例
在一些工程文件里面会有 u8 u16 u32 u64的类型,这些类型其实是二次定义的,在库函数文件里面是没有这种定义的。
#define 定义
#define u8 uint8_t;
#define u16 uint16_t;
#define u32 uint32_t;
#define u64 uint64_t;
//#define 宏名 替换文本 作用是编译到新名称的时候使用旧类型替代
typedef 定义
//定义方式1
typedef uint8_t u8;
typedef uint16_t u16;
typedef uint32_t u32;
typedef uint64_t u64;
//定义方式 2
typedef unsigned char u8;
typedef unsigned shot int u16;
typedef unsigned int u32;
typedef unsigned long long u64;
//语法规则
//typedef 原始类型 新类型
//#define 宏名 替换文本
//新类型和原始类型的顺序是反的多用用不要记混了
按位操作符
按位操作符通常被使用在数据的处理,把串行总线发送的每位数据写入一个变量,或者把存储在变量中的数据进行取出特定多少位,写入多少位,还有就是在库函数封装里面,使用按位操作来完成对单片机寄存器的特殊位进行赋值,来达到特定效果。
按位操作符基本语法示例
运算符 | 含义 | 运算符 | 含义 |
& | 按位与 | ~ | 按位取反 |
| | 按位或 | << | 左移 |
^ | 按位异或 | >> | 右移 |
同时需要注意的是,按位操作符是对二进制位进行操作,通常需要将数据换算成二进制数据,在进行操作。
#include <stdio.h>
int main() {
// 示例数据
int a = 5; // 二进制:0101
int b = 3; // 二进制:0011
// 按位与(&)
int and_result = a & b; // 0001,结果是1
printf("按位与 (a & b): %d\n", and_result);
// 按位取反(~)
int not_a = ~a; // 1010,结果是-6(补码表示)
printf("按位取反 (~a): %d\n", not_a);
// 按位或(|)
int or_result = a | b; // 0111,结果是7
printf("按位或 (a | b): %d\n", or_result);
// 按位异或(^)
int xor_result = a ^ b; // 0110,结果是6
printf("按位异或 (a ^ b): %d\n", xor_result);
// 左移(<<)
int left_shift_result = a << 1; // 1010,结果是10
printf("左移 (a << 1): %d\n", left_shift_result);
// 右移(>>)
int right_shift_result = a >> 1; // 0010,结果是2
printf("右移 (a >> 1): %d\n", right_shift_result);
return 0;
}
这里的举例比较简单,但是不要以为实际用的时候就很简单了,下面是在单总线数据结构使用按位操作进行读写数据的函数可以看一下。
//到这里单次单总线通信结束,需要校验数据 计算温湿度数据
verify_num = (value>>32)+(value>>24)+(value>>16)+(value>>8);
//变量只有8位 赋值16 24 32 只能取最后8位数据 利用这个原理 完成校验
verify_num = verify_num - (value&0xff);
if(verify_num)//校验失败退出函数
{
return 0;
}
else
{
//数据处理
humidity = (value>>32)&0xff;//湿度前8位(小数点前数据)
small_point = (value>>24)&0x00ff;//湿度后8位(小数点后数据)
small_point = small_point * 0.1;//换算为小数点
humidity = humidity + small_point;//小数前+小数后
// printf("湿度:%.2f\r\n",humidity);
temperature = (value>>16)&0x0000ff;//温度前8位(小数点前数据)
small_point = (value>>8)&0x000000ff;//温度后8位(小数点后数据)
small_point = small_point * 0.1;//换算为小数点
temperature = temperature + small_point;//小数前+小数后
// printf("温度:%.2f\r\n",temperature);
return value>>8; //返回未处理的数据
同时按位操作还能实现对寄存器特定数据位,进行写入数据 1或者数据 0,在写入的同时保留32位寄存器的其他位数据,然后之变更特定位实例代码如下。
int mani()
{
static int register;//32位类型参数
//给参数的第8位写1写法1
register |= 0x100;
//给参数的第8位写1写法2
register |=(1<<8);
//这两种写法一样的效果,以后碰见不要忘了就行
}
按位操作符库函数相关调用
void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState)
{
uint32_t tmp = 0x00, tmp1 = 0x00, tmpreg = 0x00, tmpmask = 0x00;
/* Check the parameters */
assert_param(IS_GPIO_REMAP(GPIO_Remap));
assert_param(IS_FUNCTIONAL_STATE(NewState));
if((GPIO_Remap & 0x80000000) == 0x80000000)
{
tmpreg = AFIO->MAPR2;
}
else
{
tmpreg = AFIO->MAPR;
}
tmpmask = (GPIO_Remap & DBGAFR_POSITION_MASK) >> 0x10;
tmp = GPIO_Remap & LSB_MASK;
if ((GPIO_Remap & (DBGAFR_LOCATION_MASK | DBGAFR_NUMBITS_MASK)) == (DBGAFR_LOCATION_MASK | DBGAFR_NUMBITS_MASK))
{
tmpreg &= DBGAFR_SWJCFG_MASK;
AFIO->MAPR &= DBGAFR_SWJCFG_MASK;
}
else if ((GPIO_Remap & DBGAFR_NUMBITS_MASK) == DBGAFR_NUMBITS_MASK)
{
tmp1 = ((uint32_t)0x03) << tmpmask;
tmpreg &= ~tmp1;
tmpreg |= ~DBGAFR_SWJCFG_MASK;
}
else
{
tmpreg &= ~(tmp << ((GPIO_Remap >> 0x15)*0x10));
tmpreg |= ~DBGAFR_SWJCFG_MASK;
}
if (NewState != DISABLE)
{
tmpreg |= (tmp << ((GPIO_Remap >> 0x15)*0x10));
}
if((GPIO_Remap & 0x80000000) == 0x80000000)
{
AFIO->MAPR2 = tmpreg;
}
else
{
AFIO->MAPR = tmpreg;
}
}
这个是一个STM32F1系列有的重映射函数的定义,这个函数在STM32F4系列里面是没有的,定义里面大量运用了按位操作来实现对寄存器的数据位进行赋值达到特定功能。
逻辑操作符
这里的话直讲解逻辑操作符(&&
、||
、!
)不讲比较操作符(==
、!=
、>
、<
、>=
、<=
) 逻辑操作符通常使用在各种判断里面,作为判断条件,但是在学习C语言的时候其实用的不多,大家记得更多的反而是比较操作符。
这里需要注意的是逻辑操作符不会按位操作符,逻辑操作符是用在判断条件里面的,按位操作符是对数据的使用处理。
&& | 多个条件都为真时结果为真。 |
|| | 多个条件中至少有一个为真时结果为真。 |
! | 对条件取反,真变假,假变真。 |
逻辑操作符基本语法示例
这里给一下基本语法注释都在代码里面了,可以自己看一看。
//表达式1 && 表达式2
expression1 && expression2
if (x > 0 && y < 10) //两个表达式都为真才能执行
{
printf("Both conditions are true: x > 0 and y < 10\n");
}
//表达式1 || 表达式2
expression1 || expression2
if (x > 0 || y > 10)
{
printf("At least one condition is true: x > 0 or y > 10\n");
}
//!表达式
!expression
if (!is_ready) 当is_ready的值为假的时候才能执行语句里面的函数体
{
printf("The system is not ready.\n");
}
逻辑操作符相关库函数调用
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
{
uint32_t tmpreg = 0x00;
// 检查参数有效性
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
assert_param(IS_GPIO_PIN(GPIO_InitStruct->GPIO_Pin));
assert_param(IS_GPIO_MODE(GPIO_InitStruct->GPIO_Mode));
// 设置模式
tmpreg = GPIOx->MODER;
tmpreg &= ~(GPIO_MODER_MODE0 << (GPIO_InitStruct->GPIO_Pin * 2));
tmpreg |= (GPIO_InitStruct->GPIO_Mode << (GPIO_InitStruct->GPIO_Pin * 2));
GPIOx->MODER = tmpreg;
}
预处理指令
在库函数封装的工程文件里面是使用很多的指令,通常在程序的预处理阶段执行这些指令,不和程序同时编译,常见的有头文件的引入,头文件固定宏定义,预处理指令结束标识符,提示错误预处理指令,都是比较常用的指令。
对于这些常见的指令需要知道什么意思,以及使用的语法格式,都是比较重要的。
预处理指令 | 作用 |
#include | 用于将头文件定义的源码插入到当前源文件里面 |
#define | 定义常量或者宏或者函数 |
#undef | 取消之前使用#define 定义的宏 |
#ifdef | 如果宏被定义则执行代码块 |
#ifndef | 如果宏未被定义,则编译指令代码块 |
#if | 如果表达式的值为 TRUE,则编译指定代码块 |
#elif | 在#if或#ifdef条件为false时,检查这个条件 |
#else | 当#if 或者 #ifdef的条件为false的时候,编译另一段代码 |
#endif | 预处理指令结束表述,使用预处理指令结束后必须加这个指令作为结束标识 |
#error | 编译这条语句触发编译器error,显示自己定义的错误信息,用来自检必要的宏或者参数是否正确 |
预处理指令基本语法示例
下面是预处理指令的基本语法示例,都写在代码注释里面了,可以自习学习学习。
#include 基本语法示例
#include "stdint.h"
#include <stdio.h>
//主要作用就是引入定义的头文件,这些头文件里面封装着各种各样的函数 变量
#define 基本语法示例
#define Kp 0.3 //定义宏
#define LED_State(x) GPIO_WriteBit(GPIOB,GPIO_Pin_0,x?Bit_SET:Bit_RESET)
//这里定义了一个宏函数
#undef 基本语法示例
#define LED_H_ //定义宏
#undef LED_H_ //取消宏
#ifndef LED_H_ //如果宏没有定义
#error "LED_H_ is not define !" //报错提示这条语句
#endif //预处理指令结束标识
#ifdef 基本语法示例
#define LED_H_ //定义一个宏
#ifdef LED_H_ //如果这个宏被定义执行代码块
#error "LED_H_ is define "
#endif
#ifndef 基本语法示例
#ifndef LED_PIN 如果没有定义这个宏,执行下面的语句块
#define LED_PIN 13
#endif
#if 基本语法示例
#deine LED_PIN 13
#if LED_PIN ==13
#endif
#leif 基本语法示例
#defne LED_H_ 13
#if LED_H_ == 12
#error "LED_H_ is 12"
#elif LED_H_ == 13
#error "LED_H_ is 13"
#endif
//到这里 #error 的基本语法示例和 #endif 的基本语法示例就不讲了,上面的代码也能体现出来。
//上面提供的这些有语句块能执行的预处理指令,这些语句块通常是缩进的形式出现的,如果没用缩进,#endif结束标识符上面的都是代码执行块。
预处理指令相关库函数调用
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
这个是STM32中起始文件的预处理指令部分,可以看一下。
static关键字
通常在嵌入式开发工程文件中使用 static 关键字 用两种用法 修饰变量和修饰函数 限制变量的作用域,限制这个变量只能在当前文件中使用,和调用如果在其他文件中使用extern声明这个变量,但是因为这个关键字使用static关键字声明,这中情况下是调用不了的。
延长函数内的局部变量周期,如果想要函数内的局部变量在每次调用函数的时候保留上一次调用函数的时候,给函数内局部变量赋的值,可以使用static修饰函数内部的变量。这种用法通常在按键检测,用来记录按键按下了多少次等等。
还用有一种用法就是使用static修饰函数在源文件中,被static关键字修饰的函数,在其他文件中不能被extern关键字声明调用,通常被修饰的函数,是用来测试用的。
static关键字基本语法示例
//基本使用语法
static 变量类型 修饰变量;
static long long Scanf_Flag;
//修饰变量和函数
static 返回类型 函数名(参数列表) {// 函数体};
static void Function(void){};
这里给出了基本语法还用使用示例。
static关键字相关库函数调用
两个函数分别来自 stm32f4xx_hal_adc.c 和 stm32f4xx_hal_gpio.c 这两个函数。
// static 修饰的函数,供本文件内部使用
static void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, uint32_t Channel, uint32_t Rank, uint32_t Slope) {
// 函数内部实现,操作 ADC 配置
}
// static 修饰的低级操作函数
static void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal) {
// 处理 GPIO 的写操作
}
extern关键字
这个关键字通常用于声明来自其他文件的函数或者变量,如果不是再当前.c文件中调用的,那么如果没有使用extern关键字进行生命,编译器通常会报错提示找不到函数定义,使用extern关键字通常可以不用引入头文件。
另外一种用法就是,在.c文件中定义变量,然后再.h文件中使用extern关键字声明这个变量,如果需要再其他.c文件中使用其他文件中声明的变量,只需要引入.h文件就可以了。
extern关键字基本语法示例
关键字的基本语法示例。
extern 数据类型 变量名;
extern 返回类型 函数名(参数列表);
extern int Flag;
extern void (*Carry_Function)(void);
extern库函数调用
下面是extern关键字两种的用法,第一种用法其实用的不多。
//用法1 在其它.c文件中 用extern关键字声明来自其他文件中的变量
#include <stdio.h>
int globalVar = 42; // 定义全局变量
void printMessage() { // 定义一个函数
printf("Hello from file1.c\n");
}
#include <stdio.h>
// 使用 extern 声明来自其他文件的变量和函数
extern int globalVar;
extern void printMessage();
int main() {
printf("globalVar = %d\n", globalVar); // 访问外部变量
printMessage(); // 调用外部函数
return 0;
}
这种用法在.h文件中经常使用,将整个工程中所用到的全局变量文件在.h文件中声明,然后通过引入.h文件的方法,在其他.c文件中使用这个变量。
//第二种用法
//.h文件
#ifndef GLOBAL_H
#define GLOBAL_H
extern int sharedVariable; // 声明全局变量
#endif
//在需要使用变量的.c文件直接,引入头文件进行使用
#include "global.h"
int sharedVariable = 100; // 定义全局变量
欢迎收藏,欢迎指正,希望对你,有所帮助!!!