C51学习-DAY6

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.ckey.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()
如何用定时器替代阻塞式延时

从下一天开始,程序会从"阻塞式写法"逐步进入"定时器 + 状态机"的实际项目写法。

相关推荐
YM52e1 小时前
手写模型集合书籍鸿蒙PC ArkTS 对象字面量类型问题约束深度解析
学习·华为·harmonyos·鸿蒙
西城微科方案开发1 小时前
HC89F0531-SSOP24增强型8位单片机功能特性全面解析
单片机·嵌入式硬件
hhcgchpspk2 小时前
xss漏洞学习笔记
笔记·学习·网络安全·xss
情绪总是阴雨天~2 小时前
OCR光学字符识别技术:完整原理与实战学习笔记
笔记·学习·ocr
searchforAI2 小时前
B站视频怎么转文字稿?AI自动总结要点+生成思维导图教程
人工智能·笔记·学习·ai·语音识别·知识管理·视频总结
崇山峻岭之间2 小时前
单片机步进电机梯形S形加减速实验
单片机·嵌入式硬件
只做人间不老仙2 小时前
C++ grpc 拦截器示例学习
开发语言·c++·学习
踏着七彩祥云的小丑2 小时前
Go学习第7天:Map集合 + 递归函数 + 类型转换
开发语言·学习·golang·go
me8322 小时前
【AI】Langchain4j开发学习笔记
人工智能·笔记·学习