51单片机C语言重新入门:第六天学习函数封装、模块化代码结构与.h/.c文件分离
前言
这是我重新学习 51 单片机 C 语言的第六天笔记。
前面五天主要学习了:
text
第一天:C51 程序结构、main()、while(1)、LED 闪烁
第二天:数据类型、变量、bit、unsigned char、unsigned int
第三天:sfr、sbit、寄存器、位操作
第四天:GPIO 输出控制、高低电平、IO 模式
第五天:GPIO 输入、按键读取、简单消抖
第六天开始,不再只关注"代码能不能跑",而是开始关注代码结构。
在常规小项目中,如果所有代码都写在 main.c 里,刚开始看起来很方便,但是功能一多,代码会很快变乱。
例如一个小型控制项目中可能会有:
text
LED 控制
按键读取
延时函数
蜂鸣器控制
电池检测
串口调试
PWM 控制
ADC 采样
模式切换
低功耗处理
如果所有功能都堆在一个 main.c 文件里,后期查问题、改功能、移植代码都会非常麻烦。
所以第六天开始学习:
text
函数封装
模块化编程
.h 文件
.c 文件
头文件声明
源文件实现
main.c 如何调用底层驱动
本篇文章会把前面第五天的"按键每按一次 LED 翻转一次"程序,整理成一个更规范的模块化小项目。
一、第六天学习目标
第六天的学习目标是:
text
理解为什么要模块化
理解 .h 文件和 .c 文件的区别
知道函数声明、函数定义、函数调用的区别
能写 led.h / led.c
能写 key.h / key.c
能写 delay.h / delay.c
知道 main.c 应该只写主逻辑
知道 static 的基本用法
知道 Keil 中多个 .c 文件需要添加到工程
能把一个单文件程序拆分成多个模块
今天最重要的一句话是:
模块化的本质,是把不同功能拆到不同的
.c文件中,再用.h文件提供对外接口,让main.c只负责主逻辑。
二、为什么要学习模块化?
前几天写代码时,大部分代码都放在一个 main.c 文件里。
例如:
c
#include "STC8G.H"
sbit LED = P3^7;
sbit KEY = P3^2;
void Delay_ms(unsigned int ms);
void LED_On(void);
void LED_Off(void);
bit Key_IsPressed_Debounce(void);
void main(void)
{
while(1)
{
if(Key_IsPressed_Debounce() == 1)
{
LED_On();
}
}
}
这种写法在刚开始学习时没问题。
但是随着功能越来越多,main.c 里会逐渐堆满各种代码:
text
LED 控制代码
按键读取代码
延时函数代码
蜂鸣器控制代码
电池检测代码
串口代码
PWM 代码
ADC 代码
模式处理代码
这样后期会出现几个问题:
text
main.c 文件越来越长
代码功能混在一起
查问题不方便
移植代码不方便
多人协作不方便
修改一个功能容易影响其他功能
所以实际项目中一般会把不同功能拆成不同模块。
三、模块化的通俗理解
模块化可以理解为:
把不同功能的代码分别放到不同文件里,每个文件只负责一类功能。
例如:
text
led.c 专门负责 LED
key.c 专门负责按键
delay.c 专门负责延时
main.c 专门负责主逻辑
这样以后找代码非常方便:
text
LED 出问题,就看 led.c
按键出问题,就看 key.c
延时不准,就看 delay.c
主流程不对,就看 main.c
模块化的目标就是让代码从"能跑"变成"清楚、好维护、可扩展"。
四、一个常规小项目推荐的文件结构
从第六天开始,可以把代码整理成下面这种结构:
text
Project
│
├── main.c
│
├── led.h
├── led.c
│
├── key.h
├── key.c
│
├── delay.h
├── delay.c
│
└── STC8G.H
每个文件的作用如下:
| 文件 | 作用 |
|---|---|
main.c |
主程序,只写主逻辑 |
led.h |
LED 模块对外声明 |
led.c |
LED 模块具体实现 |
key.h |
按键模块对外声明 |
key.c |
按键模块具体实现 |
delay.h |
延时函数声明 |
delay.c |
延时函数实现 |
STC8G.H |
芯片寄存器头文件 |
这样结构就清楚很多。
五、.h文件和.c文件分别是什么?
这是今天最重要的基础。
1. 什么是.h文件?
.h 文件叫头文件。
它主要负责:
text
告诉其他文件:我这里有哪些函数可以被调用。
可以把 .h 文件理解为"菜单"或"说明书"。
例如 led.h:
c
#ifndef __LED_H__
#define __LED_H__
void LED_Init(void);
void LED_On(void);
void LED_Off(void);
void LED_Toggle(void);
#endif
它告诉其他文件:
text
LED 模块有 LED_Init()
LED 模块有 LED_On()
LED 模块有 LED_Off()
LED 模块有 LED_Toggle()
但是它不写这些函数具体怎么实现。
2. 什么是.c文件?
.c 文件叫源文件。
它主要负责:
text
真正实现函数功能。
可以把 .c 文件理解为"厨房"。
例如 led.c:
c
#include "STC8G.H"
#include "led.h"
#define BIT7 0x80
#define LED_ON_LEVEL 0
#define LED_OFF_LEVEL 1
sbit LED = P3^7;
static bit led_state = 0;
void LED_Init(void)
{
LED_Off();
P3M1 &= ~BIT7;
P3M0 |= BIT7;
}
void LED_On(void)
{
LED = LED_ON_LEVEL;
led_state = 1;
}
void LED_Off(void)
{
LED = LED_OFF_LEVEL;
led_state = 0;
}
void LED_Toggle(void)
{
if(led_state == 1)
{
LED_Off();
}
else
{
LED_On();
}
}
这里才是真正控制 LED 的代码。
六、.h和.c的通俗比喻
可以这样理解:
text
.h 文件:说明书 / 菜单 / 对外接口
.c 文件:具体实现 / 厨房 / 内部细节
比如去饭店点菜。
菜单上写:
text
宫保鸡丁
鱼香肉丝
米饭
你只需要知道菜单上有什么菜,不需要知道厨房怎么切菜、怎么炒菜。
.h 文件就像菜单。
.c 文件就像厨房。
main.c 只需要看菜单调用函数,不需要关心 LED 具体接在哪个引脚、按键按下到底是高电平还是低电平。
七、为什么main.c不应该关心太多底层细节?
不推荐在 main.c 里到处写:
c
LED = 0;
P3M1 &= ~0x80;
P3M0 |= 0x80;
if(KEY == 0)
{
}
因为这样 main.c 会和硬件细节强绑定。
更推荐让 main.c 写成:
c
LED_Init();
Key_Init();
if(Key_IsPressed_Debounce() == 1)
{
LED_Toggle();
}
这样主函数读起来更像自然语言:
text
初始化 LED
初始化按键
如果按键被按下
就翻转 LED
这就是模块化的好处。
八、头文件里的include guard是什么?
头文件里经常会看到这样的写法:
c
#ifndef __LED_H__
#define __LED_H__
void LED_Init(void);
void LED_On(void);
void LED_Off(void);
void LED_Toggle(void);
#endif
这叫:
text
头文件保护
或者叫:
text
include guard
它的作用是:
防止同一个头文件被重复包含。
1. include guard的通俗理解
假设 led.h 被多个文件包含。
如果没有保护,编译器可能会重复看到同样的声明,在复杂项目中容易出问题。
所以写:
c
#ifndef __LED_H__
#define __LED_H__
意思是:
text
如果之前没有定义过 __LED_H__,那就继续往下包含。
最后:
c
#endif
表示结束。
初学阶段先记住:
text
每个 .h 文件都建议加 include guard。
九、函数声明、函数定义、函数调用
模块化里一定要分清:
text
函数声明
函数定义
函数调用
1. 函数声明
函数声明一般写在 .h 文件里。
例如:
c
void LED_On(void);
意思是:
text
告诉其他文件:有一个函数叫 LED_On,它没有参数,也没有返回值。
注意:
text
函数声明有分号,没有大括号。
2. 函数定义
函数定义一般写在 .c 文件里。
例如:
c
void LED_On(void)
{
LED = LED_ON_LEVEL;
}
意思是:
text
真正实现 LED_On() 这个函数。
注意:
text
函数定义有大括号,里面有具体代码。
3. 函数调用
函数调用一般写在 main.c 或其他 .c 文件里。
例如:
c
LED_On();
意思是:
text
使用 LED_On() 这个函数。
十、今天的小项目目标
今天把第五天的程序模块化。
功能仍然很简单:
text
LED 接 P3.7,低电平点亮
KEY 接 P3.2,按下为低电平
按键每按一次,LED 状态翻转一次
但是今天不再把所有代码放在一个文件里。
我们拆成:
text
delay.h / delay.c
led.h / led.c
key.h / key.c
main.c
十一、delay模块
延时函数很多地方都会用到。
例如:
text
LED 闪烁需要延时
按键消抖需要延时
等待松手后需要延时
所以把它单独做成 delay 模块。
1. delay.h
c
#ifndef __DELAY_H__
#define __DELAY_H__
void Delay_ms(unsigned int ms);
#endif
解释:
text
delay.h 只声明 Delay_ms() 函数。
其他文件只要包含 delay.h,就可以调用 Delay_ms()。
2. delay.c
c
#include "delay.h"
void Delay_ms(unsigned int ms)
{
unsigned int i;
unsigned int j;
for(i = 0; i < ms; i++)
{
for(j = 0; j < 1000; j++)
{
;
}
}
}
解释:
text
delay.c 负责真正实现 Delay_ms()。
注意:
text
这个延时函数还是粗略延时,不精确。
正式项目后面可以用定时器替代。
十二、LED模块
LED 模块负责:
text
LED 初始化
LED 点亮
LED 熄灭
LED 翻转
1. led.h
c
#ifndef __LED_H__
#define __LED_H__
void LED_Init(void);
void LED_On(void);
void LED_Off(void);
void LED_Toggle(void);
#endif
这个文件告诉外部:
text
LED 模块提供 4 个函数:
LED_Init()
LED_On()
LED_Off()
LED_Toggle()
2. led.c
c
#include "STC8G.H"
#include "led.h"
#define BIT7 0x80
#define LED_ON_LEVEL 0
#define LED_OFF_LEVEL 1
sbit LED = P3^7;
static bit led_state = 0;
void LED_Init(void)
{
LED_Off();
/* P3.7 设置为推挽输出 */
P3M1 &= ~BIT7;
P3M0 |= BIT7;
}
void LED_On(void)
{
LED = LED_ON_LEVEL;
led_state = 1;
}
void LED_Off(void)
{
LED = LED_OFF_LEVEL;
led_state = 0;
}
void LED_Toggle(void)
{
if(led_state == 1)
{
LED_Off();
}
else
{
LED_On();
}
}
十三、led.c逐段解释
1. 包含头文件
c
#include "STC8G.H"
#include "led.h"
STC8G.H 的作用是:
text
让编译器认识 P3、P3M0、P3M1、sbit 等芯片相关内容。
led.h 的作用是:
text
让 led.c 中的函数定义和对外声明保持一致。
2. 定义位掩码
c
#define BIT7 0x80
因为 P3.7 对应 bit7。
而 bit7 对应:
text
0x80 = 1000 0000
3. 定义LED有效电平
c
#define LED_ON_LEVEL 0
#define LED_OFF_LEVEL 1
表示:
text
LED 低电平点亮
LED 高电平熄灭
如果硬件改成高电平点亮,只需要改成:
c
#define LED_ON_LEVEL 1
#define LED_OFF_LEVEL 0
4. 定义LED引脚
c
sbit LED = P3^7;
表示:
text
LED 接在 P3.7。
5. led_state为什么加static?
c
static bit led_state = 0;
这里有一个新知识:static。
在 led.c 文件里定义:
c
static bit led_state = 0;
意思是:
text
led_state 这个变量只在 led.c 内部使用。
其他 .c 文件不能直接访问它。
通俗理解:
text
static 让变量变成"本模块内部私有变量"。
为什么这么做?
因为 led_state 是 LED 模块的内部状态。
外部只需要调用:
c
LED_On();
LED_Off();
LED_Toggle();
不应该直接修改 led_state。
这样可以防止其他文件乱改 LED 状态。
十四、什么是static?
static 在 C 语言里有多种用法。
今天先只掌握一种:
在
.c文件全局位置定义变量时,加static表示这个变量只在当前文件内部有效。
例如:
c
static bit led_state = 0;
它只属于 led.c。
main.c 不能直接访问它。
这是一种很好的模块化习惯。
1. 不加static会怎样?
如果写成:
c
bit led_state = 0;
这个变量就有可能被其他文件通过 extern 访问。
项目大了以后,别人可能在别的文件里改它:
c
led_state = 1;
这会破坏 LED 模块自己的管理逻辑。
所以内部变量尽量加 static。
十五、KEY模块
按键模块负责:
text
按键初始化
判断按键是否按下
按键消抖
等待按键松开
1. key.h
因为 bit 是 C51 中常用的数据类型,所以这里在 key.h 中包含 STC8G.H。
c
#ifndef __KEY_H__
#define __KEY_H__
#include "STC8G.H"
void Key_Init(void);
bit Key_IsPressed(void);
bit Key_IsPressed_Debounce(void);
void Key_WaitRelease(void);
#endif
这个文件告诉外部:
text
按键模块提供 4 个函数:
Key_Init()
Key_IsPressed()
Key_IsPressed_Debounce()
Key_WaitRelease()
2. key.c
c
#include "STC8G.H"
#include "key.h"
#include "delay.h"
#define BIT2 0x04
#define KEY_PRESS_LEVEL 0
sbit KEY = P3^2;
void Key_Init(void)
{
/* P3.2 设置为高阻输入 */
P3M1 |= BIT2;
P3M0 &= ~BIT2;
}
bit Key_IsPressed(void)
{
if(KEY == KEY_PRESS_LEVEL)
{
return 1;
}
else
{
return 0;
}
}
bit Key_IsPressed_Debounce(void)
{
if(KEY == KEY_PRESS_LEVEL)
{
Delay_ms(20);
if(KEY == KEY_PRESS_LEVEL)
{
return 1;
}
}
return 0;
}
void Key_WaitRelease(void)
{
while(KEY == KEY_PRESS_LEVEL)
{
;
}
Delay_ms(20);
}
十六、key.c逐段解释
1. 为什么key.c要包含delay.h?
c
#include "delay.h"
因为 Key_IsPressed_Debounce() 里面用到了:
c
Delay_ms(20);
如果不包含 delay.h,编译器可能不知道 Delay_ms() 是什么函数。
所以:
text
哪个 .c 文件用到了某个模块的函数,就包含对应的 .h 文件。
2. 按键引脚定义
c
sbit KEY = P3^2;
表示:
text
按键接在 P3.2。
3. 按键有效电平
c
#define KEY_PRESS_LEVEL 0
表示:
text
按键按下时,IO 为低电平。
如果以后硬件改为按下高电平,只需要改成:
c
#define KEY_PRESS_LEVEL 1
4. Key_Init()
c
void Key_Init(void)
{
P3M1 |= BIT2;
P3M0 &= ~BIT2;
}
表示:
text
把 P3.2 配置成高阻输入。
P3.2 对应:
text
BIT2 = 0x04
高阻输入要求:
text
P3M1.2 = 1
P3M0.2 = 0
十七、main.c应该怎么写?
有了 LED 模块、按键模块、延时模块后,main.c 就可以写得非常清爽。
c
#include "STC8G.H"
#include "led.h"
#include "key.h"
void main(void)
{
LED_Init();
Key_Init();
while(1)
{
if(Key_IsPressed_Debounce() == 1)
{
LED_Toggle();
Key_WaitRelease();
}
}
}
这个 main.c 读起来就是:
text
初始化 LED
初始化按键
循环执行:
如果按键被确认按下
LED 翻转一次
等待按键松开
可以看到:
text
main.c 里没有 P3M0
main.c 里没有 P3M1
main.c 里没有 sbit LED = P3^7
main.c 里没有 if(KEY == 0)
这些硬件细节都放到了 led.c 和 key.c 里面。
这就是模块化的意义。
十八、完整模块化代码汇总
下面把完整代码按文件列出来。
1. delay.h
c
#ifndef __DELAY_H__
#define __DELAY_H__
void Delay_ms(unsigned int ms);
#endif
2. delay.c
c
#include "delay.h"
void Delay_ms(unsigned int ms)
{
unsigned int i;
unsigned int j;
for(i = 0; i < ms; i++)
{
for(j = 0; j < 1000; j++)
{
;
}
}
}
3. led.h
c
#ifndef __LED_H__
#define __LED_H__
void LED_Init(void);
void LED_On(void);
void LED_Off(void);
void LED_Toggle(void);
#endif
4. led.c
c
#include "STC8G.H"
#include "led.h"
#define BIT7 0x80
#define LED_ON_LEVEL 0
#define LED_OFF_LEVEL 1
sbit LED = P3^7;
static bit led_state = 0;
void LED_Init(void)
{
LED_Off();
/* P3.7 设置为推挽输出 */
P3M1 &= ~BIT7;
P3M0 |= BIT7;
}
void LED_On(void)
{
LED = LED_ON_LEVEL;
led_state = 1;
}
void LED_Off(void)
{
LED = LED_OFF_LEVEL;
led_state = 0;
}
void LED_Toggle(void)
{
if(led_state == 1)
{
LED_Off();
}
else
{
LED_On();
}
}
5. key.h
c
#ifndef __KEY_H__
#define __KEY_H__
#include "STC8G.H"
void Key_Init(void);
bit Key_IsPressed(void);
bit Key_IsPressed_Debounce(void);
void Key_WaitRelease(void);
#endif
6. key.c
c
#include "STC8G.H"
#include "key.h"
#include "delay.h"
#define BIT2 0x04
#define KEY_PRESS_LEVEL 0
sbit KEY = P3^2;
void Key_Init(void)
{
/* P3.2 设置为高阻输入 */
P3M1 |= BIT2;
P3M0 &= ~BIT2;
}
bit Key_IsPressed(void)
{
if(KEY == KEY_PRESS_LEVEL)
{
return 1;
}
else
{
return 0;
}
}
bit Key_IsPressed_Debounce(void)
{
if(KEY == KEY_PRESS_LEVEL)
{
Delay_ms(20);
if(KEY == KEY_PRESS_LEVEL)
{
return 1;
}
}
return 0;
}
void Key_WaitRelease(void)
{
while(KEY == KEY_PRESS_LEVEL)
{
;
}
Delay_ms(20);
}
7. main.c
c
#include "STC8G.H"
#include "led.h"
#include "key.h"
void main(void)
{
LED_Init();
Key_Init();
while(1)
{
if(Key_IsPressed_Debounce() == 1)
{
LED_Toggle();
Key_WaitRelease();
}
}
}
十九、Keil里如何使用多个.c文件?
在 Keil 工程中,不是只创建文件就行,还要把 .c 文件添加到工程里。
需要确保工程中包含:
text
main.c
delay.c
led.c
key.c
.h 文件不一定要单独添加到工程源码列表里,但必须放在工程目录下,确保 #include "xxx.h" 能找到。
通俗说:
text
.c 文件要参与编译,所以要添加到 Keil 工程中。
.h 文件是被 include 进来的,一般放在同一目录即可。
如果只写了 led.c,但没有添加到 Keil 工程中,可能会出现类似错误:
text
unresolved external symbol LED_Init
这通常说明:
text
led.c 没有加入工程编译。
二十、为什么.h文件里不要写函数实现?
不推荐在 .h 文件里写:
c
void LED_On(void)
{
LED = 0;
}
原因是:
text
.h 文件可能会被多个 .c 文件包含。
如果函数实现写在 .h 里,可能导致重复定义。
更推荐:
text
.h 文件只写声明
.c 文件写实现
也就是:
led.h:
c
void LED_On(void);
led.c:
c
void LED_On(void)
{
LED = LED_ON_LEVEL;
}
这是非常重要的工程习惯。
二十一、哪些内容适合放在.h文件?
.h 文件通常适合放:
text
函数声明
对外使用的宏定义
对外使用的数据类型定义
必要的 include
例如:
c
#ifndef __KEY_H__
#define __KEY_H__
#include "STC8G.H"
#define KEY_NONE 0
#define KEY_UP 1
#define KEY_DOWN 2
void Key_Init(void);
unsigned char Key_GetValue(void);
#endif
二十二、哪些内容不适合放在.h文件?
.h 文件不适合放:
text
函数具体实现
模块内部变量定义
硬件私有细节
只在一个 .c 文件中使用的宏定义
例如这些更适合放在 led.c:
c
#define LED_ON_LEVEL 0
#define LED_OFF_LEVEL 1
sbit LED = P3^7;
static bit led_state = 0;
因为它们属于 LED 模块内部细节。
二十三、什么是对外接口?
对外接口就是:
这个模块允许其他模块调用的函数。
例如 LED 模块对外接口是:
c
void LED_Init(void);
void LED_On(void);
void LED_Off(void);
void LED_Toggle(void);
这些函数写在 led.h 里。
其他文件只要写:
c
#include "led.h"
就可以调用它们。
但是其他文件不应该直接访问:
c
sbit LED = P3^7;
static bit led_state = 0;
这些是 LED 模块内部细节。
二十四、模块化后的好处
模块化有很多好处。
1. main.c更清楚
原来 main.c 里可能有很多底层寄存器操作。
模块化后:
c
void main(void)
{
LED_Init();
Key_Init();
while(1)
{
if(Key_IsPressed_Debounce() == 1)
{
LED_Toggle();
Key_WaitRelease();
}
}
}
主逻辑非常清楚。
2. 硬件改动时更好维护
如果 LED 从 P3.7 改到 P3.6,只需要修改 led.c:
c
sbit LED = P3^6;
#define BIT6 0x40
main.c 不需要改。
3. 功能可以复用
以后别的项目也需要 LED 模块,可以直接复制:
text
led.h
led.c
然后根据硬件改一下引脚即可。
4. 调试更方便
按键不工作,就查:
text
key.c
LED 不工作,就查:
text
led.c
延时不对,就查:
text
delay.c
二十五、模块之间如何调用?
模块之间通过 .h 文件联系。
例如:
main.c 要调用 LED 模块,就写:
c
#include "led.h"
main.c 要调用按键模块,就写:
c
#include "key.h"
key.c 要调用延时模块,就写:
c
#include "delay.h"
通俗理解:
text
谁要用谁,就 include 谁的头文件。
二十六、模块化时常见错误
1. 只写了.h,没有写.c
例如只写了声明:
c
void LED_On(void);
但是没有实现:
c
void LED_On(void)
{
LED = LED_ON_LEVEL;
}
结果会报错。
原因是:
text
函数只有声明,没有定义。
2. 写了.c,但没有加入Keil工程
例如写了 led.c,但是没有添加到 Keil 工程中。
结果可能会报:
text
unresolved external symbol LED_On
原因是:
text
编译器看到了函数声明,但链接器没有找到函数实现。
3. 函数声明和定义不一致
错误示例:
led.h:
c
void LED_On(void);
led.c:
c
void LED_On(unsigned char state)
{
}
这就不一致。
正确做法是:
text
.h 里的声明和 .c 里的定义必须完全匹配。
4. 头文件没有include guard
不推荐:
c
void LED_On(void);
void LED_Off(void);
推荐:
c
#ifndef __LED_H__
#define __LED_H__
void LED_On(void);
void LED_Off(void);
#endif
5. 在.h文件里定义全局变量
不推荐在 .h 文件里写:
c
unsigned char key_value = 0;
因为这个头文件如果被多个 .c 文件包含,就可能造成重复定义。
如果确实要跨文件共享变量,后面需要学习 extern。
但初学阶段更推荐:
text
尽量不要跨文件共享变量。
用函数接口传递状态。
二十七、什么是extern?
可以先简单了解一下。
extern 的意思是:
text
这个变量或函数在别的文件里定义,这里只是声明一下。
例如:
c
extern unsigned char key_value;
意思是:
text
key_value 这个变量在别的 .c 文件里定义。
我这里只是告诉编译器它存在。
但是初学阶段不建议大量使用 extern 共享变量。
更好的方式是:
text
用函数访问模块内部状态。
例如:
c
bit LED_GetState(void);
而不是让外部直接修改:
c
led_state = 1;
二十八、模块内部变量尽量static
例如:
c
static bit led_state = 0;
这样变量只在 led.c 内部有效。
外部不能直接改它。
这有两个好处:
text
防止其他文件误修改
模块边界更清楚
可以这样理解:
text
该给别人用的函数,放到 .h 里。
不想给别人碰的变量,加 static 放在 .c 里。
二十九、C51中函数封装要注意什么?
51 单片机资源有限,函数封装虽然好,但也要注意:
text
不要过度封装
不要写太深的函数调用层级
尽量不用递归
局部变量不要太多
float 运算慎用
1. 不要使用递归
递归就是函数自己调用自己。
例如:
c
void Test(void)
{
Test();
}
这种写法在单片机里非常危险。
原因是:
text
51 单片机 RAM 和堆栈资源有限。
递归容易导致栈溢出。
初学阶段记住:
text
C51 项目中不要使用递归。
2. 局部变量不要太多
例如:
c
void Test(void)
{
unsigned char buf[100];
}
在资源有限的单片机里,局部数组过大可能会造成内存问题。
所以嵌入式里要注意:
text
变量够用即可
数组大小要谨慎
大数组尽量不要随便放在函数内部
3. 函数不要做太多事情
不推荐:
c
void System_Run(void)
{
/* 处理 LED */
/* 处理按键 */
/* 处理电池 */
/* 处理串口 */
/* 处理模式 */
/* 处理保存 */
}
更推荐拆成:
c
void LED_Process(void);
void Key_Process(void);
void Battery_Process(void);
void Mode_Process(void);
一个函数只做一类事情,后期更容易维护。
三十、今天的完整小项目逻辑
今天完成的模块化小项目逻辑如下:
text
上电
初始化 LED
初始化按键
进入 while(1)
如果检测到按键按下:
消抖确认
LED 翻转一次
等待按键松开
继续循环
主函数代码:
c
void main(void)
{
LED_Init();
Key_Init();
while(1)
{
if(Key_IsPressed_Debounce() == 1)
{
LED_Toggle();
Key_WaitRelease();
}
}
}
可以看到,主函数已经非常接近自然语言。
这就是模块化代码的目标。
三十一、第六天练习任务
任务1:说出.h和.c的区别
参考答案:
text
.h 文件主要写函数声明和对外接口。
.c 文件主要写函数具体实现。
任务2:写一个led.h
要求声明:
text
LED_Init()
LED_On()
LED_Off()
LED_Toggle()
参考答案:
c
#ifndef __LED_H__
#define __LED_H__
void LED_Init(void);
void LED_On(void);
void LED_Off(void);
void LED_Toggle(void);
#endif
任务3:写一个key.h
要求声明:
text
Key_Init()
Key_IsPressed()
Key_IsPressed_Debounce()
Key_WaitRelease()
参考答案:
c
#ifndef __KEY_H__
#define __KEY_H__
#include "STC8G.H"
void Key_Init(void);
bit Key_IsPressed(void);
bit Key_IsPressed_Debounce(void);
void Key_WaitRelease(void);
#endif
任务4:解释static bit led_state = 0;
参考答案:
text
led_state 是一个 bit 类型变量,用来保存 LED 逻辑状态。
前面的 static 表示它只在当前 led.c 文件内部有效,其他文件不能直接访问。
任务5:解释为什么main.c要include led.h
参考答案:
text
因为 main.c 要调用 LED_Init()、LED_Toggle() 等 LED 模块函数。
这些函数的声明在 led.h 里,所以 main.c 需要包含 led.h。
任务6:解释为什么key.c要include delay.h
参考答案:
text
因为 key.c 中的消抖函数要调用 Delay_ms()。
Delay_ms() 的声明在 delay.h 里,所以 key.c 需要包含 delay.h。
任务7:把第五天的单文件程序拆成多个文件
目标文件结构:
text
main.c
led.h
led.c
key.h
key.c
delay.h
delay.c
要求:
text
main.c 里不直接写 LED = 0
main.c 里不直接写 if(KEY == 0)
main.c 只调用 LED 和 KEY 模块提供的函数
三十二、第六天常见错误
1. 忘记把.c文件加入Keil工程
现象:
text
编译或链接报 unresolved external symbol
解决方法:
text
确认 led.c、key.c、delay.c 都已经添加到 Keil 工程中。
2. 头文件名写错
例如文件叫:
text
delay.h
但代码写成:
c
#include "Delay.h"
有些环境对大小写不敏感,但建议保持一致。
推荐统一小写或统一风格。
3. 函数声明和定义不一致
例如:
c
void LED_Set(bit state);
但实现写成:
c
void LED_Set(unsigned char state)
{
}
这种可能引起编译错误或隐患。
4. 在多个.c文件中重复定义同一个sbit
不推荐在多个文件里都写:
c
sbit LED = P3^7;
更推荐:
text
LED 的 sbit 定义只放在 led.c 里。
其他文件通过 LED_On()、LED_Off()、LED_Toggle() 调用。
5. 在.h文件里写变量定义
不推荐:
c
bit led_state = 0;
放在 .h 文件里。
更推荐:
c
static bit led_state = 0;
放在 led.c 文件里。
三十三、第六天必须掌握的重点
今天必须掌握下面这些内容:
text
模块化就是把不同功能拆到不同文件里
.h 文件负责函数声明和对外接口
.c 文件负责函数具体实现
main.c 应该尽量只写主逻辑
谁要调用某个模块,就 include 该模块的 .h 文件
.c 文件要添加到 Keil 工程里才会参与编译
.h 文件要加 include guard
函数声明和函数定义必须一致
模块内部变量建议加 static
不要在 .h 文件里随便定义全局变量
C51 中不要使用递归
局部变量和数组大小要谨慎
三十四、第六天总结
今天学习的是函数封装和模块化代码结构。
可以用一句话总结:
模块化的本质,是把不同功能拆分到不同的
.c文件中,再用.h文件提供对外接口,让main.c只负责主逻辑。
今天最重要的理解是:
text
main.c 不应该关心 LED 具体接在哪个引脚。
main.c 不应该关心按键按下是高电平还是低电平。
main.c 只需要调用 LED 和 KEY 模块提供的函数。
第六天达到下面程度就算合格:
text
知道 .h 和 .c 的区别
能写 led.h 和 led.c
能写 key.h 和 key.c
能写 delay.h 和 delay.c
知道 main.c 如何调用模块函数
知道 include guard 的作用
知道 static 可以让变量只在本文件内部有效
能把第五天的按键控制 LED 程序拆成多个文件
后续可以继续学习:
text
定时器基础
定时器中断
1ms 系统节拍
为什么正式项目不建议一直使用 Delay_ms()
如何用定时器替代阻塞式延时
从下一天开始,程序会从"阻塞式写法"逐步进入"定时器 + 状态机"的实际项目写法。