打卡第六集-LED闪烁和花式点灯
#define MAIN_Fosc 24000000UL
这行代码定义了一个宏常量,我来详细解释其语法:
语法分解
c
#define MAIN_Fosc 24000000UL
1. #define - 预处理指令
- 这是C语言的预处理指令
- 在编译前由预处理器处理
- 用于定义宏常量或宏函数
2. MAIN_Fosc - 宏名称
- 标识符,通常使用大写字母
- 命名含义:MAIN(主)+ Fosc(振荡器频率)
- 在代码中所有出现这个名称的地方都会被替换
3. 24000000UL - 宏值(替换文本)
这是一个整型常量,其中:
24000000 - 数值部分,表示 24,000,000
U - 后缀,表示 unsigned(无符号类型)
L - 后缀,表示 long(长整型)
类型说明
| 后缀 |
含义 |
类型 |
U |
unsigned |
无符号 |
L |
long |
长整型 |
UL |
unsigned long |
无符号长整型 |
ULL |
unsigned long long |
无符号长长整型 |
实际应用
这个宏通常用于单片机/嵌入式开发中,定义主时钟频率:
c
#define MAIN_Fosc 24000000UL // 主频 24MHz
// 使用示例
void delay_us(unsigned int us) {
unsigned int count = MAIN_Fosc / 1000000 * us;
while(count--);
}
// 预处理器处理后:
void delay_us(unsigned int us) {
unsigned int count = 24000000UL / 1000000 * us;
while(count--);
}
优点
- 提高可读性 - 名称说明含义
- 便于维护 - 修改频率只需改一处
- 类型安全 -
UL后缀确保计算时使用正确的数据类型
- 避免魔法数字 - 24,000,000有明确含义
这种写法在STM32、8051等单片机固件开发中非常常见。
while 与 do-while 循环的区别
1. 语法结构
while 循环
c
while (条件表达式) {
// 循环体
// 先判断条件,后执行
}
do-while 循环
c
do {
// 循环体
// 先执行一次,后判断条件
} while (条件表达式); // 注意这里有分号
2. 核心区别
| 特性 |
while |
do-while |
| 执行顺序 |
先判断,后执行 |
先执行,后判断 |
| 最少执行次数 |
0次 |
1次 |
| 适用场景 |
可能一次都不执行 |
至少执行一次 |
3. 代码示例
示例1:最少执行次数
c
// while - 可能执行0次
int i = 10;
while (i < 5) {
printf("while: %d\n", i); // 不会执行
i++;
}
// do-while - 至少执行1次
int j = 10;
do {
printf("do-while: %d\n", j); // 会执行1次
j++;
} while (j < 5);
示例2:实际应用场景
while 场景 - 输入验证(可能一次都不需要)
c
int value;
printf("请输入正数:");
scanf("%d", &value);
while (value <= 0) {
printf("输入错误,请重新输入:");
scanf("%d", &value);
}
do-while 场景 - 菜单程序(至少显示一次菜单)
c
int choice;
do {
printf("\n=== 菜单 ===\n");
printf("1. 开始游戏\n");
printf("2. 设置\n");
printf("3. 退出\n");
printf("请选择:");
scanf("%d", &choice);
switch(choice) {
case 1: printf("游戏开始\n"); break;
case 2: printf("设置界面\n"); break;
case 3: printf("退出游戏\n"); break;
default: printf("无效选择\n");
}
} while (choice != 3);
4. 单片机嵌入式场景
结合你的 MAIN_Fosc 宏定义:
c
// while 场景 - 等待标志位(可能立即满足)
volatile unsigned char flag = 0;
// 等待中断标志,但如果标志已经为1,直接跳过循环
while (!flag) {
// 等待中断发生
}
// do-while 场景 - 至少执行一次延时
void delay_us(unsigned int us) {
unsigned int count;
do {
count = MAIN_Fosc / 1000000;
while (count--);
} while (--us); // 至少延时1微秒
}
5. 选择建议
| 场景 |
推荐 |
原因 |
| 需要先判断条件 |
while |
可能不需要执行循环体 |
| 至少需要执行一次 |
do-while |
保证循环体至少执行一次 |
| 菜单选择 |
do-while |
至少显示一次菜单 |
| 文件读取 |
while |
文件可能为空 |
| 用户输入验证 |
do-while |
至少要求输入一次 |
模块化编程中函数使用的三个步骤
一、函数定义
定义函数的具体实现,包括函数要执行的功能。
语法格式:
c
返回值类型 函数名称(入口参数)
{
// 函数体:要执行的功能代码
}
要点说明:
| 组成部分 |
说明 |
示例 |
| 返回值 |
没有返回值时使用 void |
void、int、char |
| 函数名称 |
避开关键词,不重复,不使用特殊字符 |
delay_ms、getValue |
| 入口参数 |
格式:类型 名称,多个参数用逗号分隔,无参数填 void |
int x、int a, int b、void |
示例:
c
// 有返回值,有参数
int add(int a, int b)
{
return a + b;
}
// 无返回值,有参数
void delay_ms(unsigned int ms)
{
// 延时功能代码
}
// 无返回值,无参数
void led_toggle(void)
{
// LED翻转功能代码
}
二、函数声明
在使用函数前告知编译器函数的存在,通常放在头文件(.h)或源文件(.c)开头。
语法格式:
c
返回值类型 函数名称(入口参数);
要点:
- 与函数定义的第一行完全相同,末尾加分号
;
- 声明不包含函数体
- 通常放在
.h 头文件中
示例:
c
// 函数声明
int add(int a, int b);
void delay_ms(unsigned int ms);
void led_toggle(void);
三、函数调用
在程序中执行函数。
语法格式:
c
函数名称(入口参数);
要点说明:
| 调用方式 |
格式 |
示例 |
| 无返回值调用 |
函数名(参数); |
delay_ms(100); |
| 有返回值调用 |
变量 = 函数名(参数); |
result = add(3, 5); |
| 无参数调用 |
函数名(); 或 函数名(void); |
led_toggle(); |
示例:
c
void main(void)
{
int sum;
// 无返回值函数调用
delay_ms(100);
led_toggle();
// 有返回值函数调用
sum = add(10, 20); // sum = 30
}
完整示例(模块化编程)
头文件 math_utils.h
c
#ifndef _MATH_UTILS_H_
#define _MATH_UTILS_H_
// 函数声明
int add(int a, int b);
int multiply(int a, int b);
#endif
源文件 math_utils.c
c
#include "math_utils.h"
// 函数定义
int add(int a, int b)
{
return a + b;
}
int multiply(int a, int b)
{
return a * b;
}
主文件 main.c
c
#include <stdio.h>
#include "math_utils.h"
int main(void)
{
int result;
// 函数调用
result = add(5, 3); // result = 8
result = multiply(4, 6); // result = 24
return 0;
}
快速记忆口诀
定义写实现,声明放头前
调用传参数,注意返回值
| 步骤 |
作用 |
位置 |
| 定义 |
实现函数功能 |
.c 源文件 |
| 声明 |
告诉编译器函数存在 |
.h 头文件 |
| 调用 |
执行函数 |
需要使用的 .c 文件中 |
.h文件(头文件)的作用
一、什么是.h文件
头文件(Header File) 是以 .h 为扩展名的文件,用于存放程序中需要共享的声明信息,相当于一个 接口说明书。
二、.h文件的核心作用
| 作用 |
说明 |
示例 |
| 函数声明 |
告诉其他文件有哪些函数可用 |
void delay_ms(unsigned int ms); |
| 宏定义 |
定义常量和宏 |
#define MAIN_Fosc 24000000UL |
| 类型定义 |
定义结构体、枚举等自定义类型 |
typedef struct { int x; } Point; |
| 变量声明 |
声明全局变量(extern) |
extern int system_status; |
| 包含其他头文件 |
引入依赖 |
#include <stdio.h> |
三、为什么需要.h文件
场景:没有头文件时
c
// main.c
void delay_ms(unsigned int ms); // 必须手动声明,每个文件都要写
int main(void)
{
delay_ms(100);
return 0;
}
问题:
- ❌ 每个使用
delay_ms 的文件都要重复声明
- ❌ 函数修改时,所有地方的声明都要改
- ❌ 代码冗长,难以维护
场景:使用头文件后
c
// delay.h - 头文件
#ifndef _DELAY_H_
#define _DELAY_H_
void delay_ms(unsigned int ms); // 只声明一次
#endif
// main.c
#include "delay.h" // 一行搞定
int main(void)
{
delay_ms(100);
return 0;
}
// other.c
#include "delay.h" // 其他文件也只需一行
void other_func(void)
{
delay_ms(50);
}
优点:
- ✅ 声明只写一次,多处复用
- ✅ 函数接口统一管理
- ✅ 修改只需改一处
四、.h文件的内容结构
c
// ============================================
// 头文件的标准结构
// ============================================
#ifndef _文件名_H_ // 头文件保护(防止重复包含)
#define _文件名_H_
// 1. 包含其他头文件
#include <stdio.h>
#include "other.h"
// 2. 宏定义
#define MAIN_Fosc 24000000UL
#define LED_ON 1
#define LED_OFF 0
// 3. 类型定义
typedef struct {
int x;
int y;
} Point;
typedef enum {
STATE_IDLE,
STATE_RUNNING,
STATE_STOP
} SystemState;
// 4. 全局变量声明(extern)
extern int g_system_tick;
extern SystemState g_state;
// 5. 函数声明
void system_init(void);
void delay_ms(unsigned int ms);
int add(int a, int b);
#endif // 结束头文件保护
五、.h 与 .c 的分工
| 文件类型 |
存放内容 |
作用 |
| .h 头文件 |
声明(接口) |
告诉别人"我能做什么" |
| .c 源文件 |
定义(实现) |
告诉编译器"我怎么做" |
类比理解:
- .h文件 = 餐馆的 菜单(告诉你能点什么菜)
- .c文件 = 后厨的 做法(具体怎么做这道菜)
六、模块化编程示例
项目结构
text
project/
├── main.c // 主程序
├── led.c // LED功能实现
├── led.h // LED功能声明
├── delay.c // 延时功能实现
└── delay.h // 延时功能声明
led.h(头文件)
c
#ifndef _LED_H_
#define _LED_H_
#include "delay.h" // LED需要使用延时
// 宏定义
#define LED_PIN P1_0
#define LED_ON 0
#define LED_OFF 1
// 函数声明
void led_init(void);
void led_on(void);
void led_off(void);
void led_blink(unsigned int ms);
#endif
led.c(源文件)
c
#include "led.h"
// 函数定义(实现)
void led_init(void)
{
LED_PIN = LED_OFF; // 初始关闭
}
void led_on(void)
{
LED_PIN = LED_ON;
}
void led_off(void)
{
LED_PIN = LED_OFF;
}
void led_blink(unsigned int ms)
{
led_on();
delay_ms(ms); // 调用延时函数
led_off();
delay_ms(ms);
}
main.c(主程序)
c
#include "led.h" // 只需包含头文件
void main(void)
{
led_init(); // 调用LED功能
while(1) {
led_blink(500); // 500ms闪烁
}
}
七、头文件保护机制
防止重复包含:
c
// 方法1:使用 #ifndef(最常用)
#ifndef _LED_H_
#define _LED_H_
// 头文件内容...
#endif
// 方法2:使用 #pragma once(编译器扩展)
#pragma once
// 头文件内容...
为什么需要:
c
// 错误示例:没有头文件保护
// a.h
struct Point { int x; int y; };
// b.h
#include "a.h"
// main.c
#include "a.h"
#include "b.h" // 错误!struct Point 被定义了两次
八、头文件使用总结
| 内容 |
放在.h |
放在.c |
说明 |
| 函数声明 |
✓ |
|
公开接口 |
| 函数定义 |
|
✓ |
具体实现 |
| 宏定义 |
✓ |
|
常量共享 |
| 类型定义 |
✓ |
|
结构体、枚举等 |
| 全局变量声明 |
extern int x; |
|
声明放在.h |
| 全局变量定义 |
|
int x; |
定义放在.c |
| static 函数/变量 |
|
✓ |
仅本文件使用 |
九、常见问题
Q:.h文件需要编译吗?
A:不需要,头文件通过 #include 被包含到 .c 文件中,随 .c 文件一起编译。
Q:函数声明可以省略吗?
A:如果函数调用在定义之后,可以省略;但通常建议都在 .h 中声明。
Q:.h文件可以包含.c文件吗?
A:可以但不推荐,会导致代码混乱和重复编译。
快速记忆
头文件是接口说明书,
放声明不放实现,
加保护防重复,
模块化编程离不开。