C51单片机复习-DAY2
下面这版已经整理成适合 CSDN Markdown 的文章格式,可以直接复制粘贴发布。我已把项目背景统一改成了“常规小项目 / 小型控制项目”,没有使用你要求避开的那个词。

51单片机C语言重新入门:第二天学习C51数据类型、变量与嵌入式编程习惯

前言

这是我重新学习 51 单片机 C 语言的第二天笔记。

第一天主要学习了 51 单片机 C 程序的基本结构,包括 main()while(1)sbit、IO 高低电平控制以及 LED 闪烁程序。

第二天开始进入 C 语言基础部分,不过这里不会单纯讲语法,而是结合 51 单片机实际开发场景来理解数据类型、变量、常量、溢出和嵌入式编程习惯。

在单片机程序中,数据类型的选择非常重要。因为单片机资源有限,变量类型选得是否合适,会直接影响程序的 RAM 占用、Flash 占用、运行速度和稳定性。

本篇文章主要记录以下内容:

  • C51 常用数据类型;
  • bitunsigned charunsigned int 的典型用法;
  • 为什么 51 单片机中要少用 float
  • 全局变量和局部变量;
  • 宏定义常量;
  • 数据溢出问题;
  • 整数运算中的注意事项;
  • 如何在一个常规小项目中合理定义变量。

一、为什么单片机里要特别重视数据类型?

在电脑上写程序时,可能经常随手定义变量:

int a;
float b;
double c;

一般问题不大,因为电脑的内存很大,CPU 性能也很强。

但是 51 单片机不一样。

51 单片机资源比较有限,尤其是传统 8051 架构,RAM、Flash 和运算能力都比较宝贵。

所以在 51 单片机程序中,数据类型选得是否合适,会直接影响:

程序占用 RAM 大小
程序运行速度
代码占用 Flash 大小
是否容易出现溢出
程序是否稳定

例如,亮度等级只有 0 到 20,如果定义成:

unsigned char brightness;

就已经完全够用了。

如果定义成:

long brightness;

虽然程序也可能能运行,但是明显浪费资源。

所以嵌入式 C 语言里有一个很重要的习惯:

变量类型够用就好,不要盲目使用大类型。


二、C51常用数据类型总表

51 单片机 C 语言常见数据类型如下:

数据类型 位宽 取值范围 常见用途
bit 1 bit 0 或 1 标志位
char 8 bit -128 到 127 小范围有符号数
unsigned char 8 bit 0 到 255 亮度、模式、状态、计数
int 16 bit -32768 到 32767 一般有符号整数
unsigned int 16 bit 0 到 65535 延时、计数、ADC 原始值
long 32 bit -2147483648 到 2147483647 大范围计算
unsigned long 32 bit 0 到 4294967295 大范围无符号计算
float 32 bit 小数 小数计算,51 中慎用

对于 51 单片机初学阶段,最常用的是下面三个:

unsigned char
unsigned int
bit

可以先记住下面这句话:

状态、等级、模式:unsigned char
计数、时间、ADC:unsigned int
开关、标志:bit

三、bit类型:最适合做开关标志

1. bit是什么?

bit 是 C51 特有的数据类型,用来表示 1 位数据。

它只有两个值:

0
1

所以它非常适合表示一些开关类状态,例如:

开 / 关
真 / 假
有 / 无
按下 / 未按下
低电量 / 正常电量
功能开启 / 功能关闭

示例:

bit key_pressed = 0;
bit low_battery_flag = 0;
bit power_on_flag = 0;
bit function_enable = 1;

这些变量的含义分别是:

key_pressed:按键是否按下
low_battery_flag:是否低电量
power_on_flag:系统是否开机
function_enable:某个功能是否使能

2. bit的通俗理解

可以把 bit 理解成一个小开关:

0:关
1:开

例如:

bit led_state = 0;

表示:

LED 当前状态为关闭

如果后面写:

led_state = 1;

表示:

LED 当前状态为打开

3. bit的典型用法

例如低电量报警:

bit low_battery_flag = 0;

void Check_Battery(void)
{
    if(battery_voltage < 2700)
    {
        low_battery_flag = 1;
    }
    else
    {
        low_battery_flag = 0;
    }
}

这段代码可以理解为:

如果电池电压低于 2700mV,就把低电量标志置 1;
否则把低电量标志清 0。

后面的主程序可以根据这个标志决定是否执行报警逻辑:

if(low_battery_flag == 1)
{
    // 执行低电量报警逻辑
}

四、unsigned char:51单片机中最常用的类型

1. unsigned char是什么?

unsigned char 是无符号 8 位整数。

它的范围是:

0 ~ 255

它不能表示负数。

在 51 单片机中,unsigned char 非常常用,因为 51 单片机是 8 位架构,处理 8 位数据比较自然。

它适合表示:

亮度等级
模式编号
按键编号
状态编号
小范围计数值
数组下标
PWM 占空比

2. 示例:亮度等级

假设一个小型控制项目里有亮度调节功能:

亮度等级范围:0 ~ 20

那么亮度变量可以这样定义:

unsigned char brightness = 0;

因为亮度最大也只有 20,unsigned char 的范围是 0 到 255,完全够用。

没有必要写成:

int brightness;

更没有必要写成:

long brightness;

3. 示例:模式变量

假设系统有两种工作模式:

0:普通模式
1:低功耗模式

可以定义:

unsigned char work_mode = 0;

使用时:

if(work_mode == 0)
{
    // 普通模式
}
else if(work_mode == 1)
{
    // 低功耗模式
}

不过更推荐使用宏定义,让代码更清晰:

#define MODE_NORMAL     0
#define MODE_LOW_POWER  1

unsigned char work_mode = MODE_NORMAL;

这样判断时可以写成:

if(work_mode == MODE_NORMAL)
{
    // 普通模式
}
else if(work_mode == MODE_LOW_POWER)
{
    // 低功耗模式
}

这种写法比直接写 01 更容易理解。


4. 示例:按键状态

可以定义按键值:

#define KEY_NONE    0
#define KEY_UP      1
#define KEY_DOWN    2
#define KEY_MODE    3

unsigned char key_value = KEY_NONE;

后面可以这样使用:

if(key_value == KEY_UP)
{
    brightness++;
}
else if(key_value == KEY_DOWN)
{
    brightness--;
}
else if(key_value == KEY_MODE)
{
    work_mode = MODE_LOW_POWER;
}

使用宏定义后,代码可读性更好。

如果直接写:

if(key_value == 1)

以后再看代码时可能会忘记 1 代表什么。

而写成:

if(key_value == KEY_UP)

就很清楚。


五、char和unsigned char的区别

1. char

char 是 8 位有符号整数。

它的范围通常是:

-128 ~ 127

它可以表示负数。

例如:

char temperature = -10;

表示温度为 -10。


2. unsigned char

unsigned char 是 8 位无符号整数。

它的范围是:

0 ~ 255

它不能表示负数。

例如:

unsigned char brightness = 20;

表示亮度等级为 20。


3. 通俗区别

可以这样理解:

char:可以表示负数,但是正数范围小一些;
unsigned char:不能表示负数,但是正数范围大一些。

如果变量不会出现负数,优先使用:

unsigned char

例如:

亮度等级不会是 -1
模式编号不会是 -1
按键编号不会是 -1
PWM 占空比不会是 -1

这些变量都适合用 unsigned char


六、unsigned int:适合时间、计数和ADC

1. unsigned int是什么?

unsigned int 是无符号 16 位整数。

它的范围是:

0 ~ 65535

它适合表示:

延时时间
定时器计数
毫秒计数
ADC 原始值
电压值 mV
较大的循环变量

2. 示例:延时函数参数

第一天学习过一个简单延时函数:

void Delay_ms(unsigned int ms)
{
    unsigned int i;
    unsigned int j;

    for(i = 0; i < ms; i++)
    {
        for(j = 0; j < 1000; j++)
        {
            ;
        }
    }
}

这里 ms 使用 unsigned int 比较合适。

因为实际调用时可能会写:

Delay_ms(1000);
Delay_ms(2000);
Delay_ms(5000);

这些数值已经超过了 unsigned char 的最大范围 255。

如果 ms 写成:

unsigned char ms

那么:

Delay_ms(500);

就会出问题,因为 unsigned char 最大只能表示 255,500 装不进去。


3. 示例:ADC原始值

假设 ADC 是 12 位,则原始值范围是:

0 ~ 4095

这个范围超过了 unsigned char 的最大值 255,所以要用:

unsigned int adc_value = 0;

如果电池电压用 mV 表示,比如:

3000mV

也应该使用:

unsigned int battery_mv = 3000;

而不是 unsigned char


七、int和unsigned int的区别

1. int

int 是有符号 16 位整数。

范围通常是:

-32768 ~ 32767

它可以表示负数。

例如:

int offset = -20;

2. unsigned int

unsigned int 是无符号 16 位整数。

范围是:

0 ~ 65535

它不能表示负数,但正数范围更大。


3. 单片机中怎么选?

如果数值可能为负,用:

int

例如:

int temperature = -10;
int error_value = -5;

如果数值不会为负,用:

unsigned int

例如:

unsigned int time_ms = 1000;
unsigned int adc_value = 2048;
unsigned int battery_mv = 3000;

嵌入式程序里很多变量都不会是负数,所以 unsigned int 很常用。


八、long和unsigned long

1. long

long 是 32 位有符号整数。

它的范围比较大。

例如:

long result = 0;

适合用于较大范围计算。


2. unsigned long

unsigned long 是 32 位无符号整数。

它的范围是:

0 ~ 4294967295

适合大范围无符号计数。


3. 什么时候需要用long?

比如 ADC 电压换算:

battery_mv = adc_value * 3300 / 4095;

如果 adc_value 最大为 4095,那么中间乘法结果为:

4095 × 3300 = 13513500

这个中间结果已经超过了 unsigned int 的最大值 65535。

所以更安全的写法是:

unsigned int adc_value;
unsigned int battery_mv;

battery_mv = (unsigned long)adc_value * 3300 / 4095;

这里:

(unsigned long)adc_value

表示先把 adc_value 转成 unsigned long,再参与计算。

这样可以防止中间乘法溢出。


4. 通俗理解

虽然最终结果可能只有 3300mV,不算大。

但是计算过程中会先做乘法:

4095 × 3300 = 13513500

这个中间值很大。

所以在嵌入式程序中要注意:

不仅要看最终结果,还要看中间计算是否会溢出。

九、float:51单片机里尽量少用

1. float是什么?

float 是小数类型。

例如:

float voltage = 3.3;

看起来很方便。


2. 为什么51里面不推荐频繁使用float?

因为 51 单片机没有像电脑 CPU 那样强大的浮点运算能力。

使用 float 会带来几个问题:

运算速度慢
占用代码空间大
占用 RAM 多
调试时容易引入误差

所以在 51 单片机中,通常不建议大量使用 float


3. 电压小数怎么表示?

例如电池电压是:

3.30V

不推荐这样写:

float battery_voltage = 3.30;

更推荐使用整数表示 mV:

unsigned int battery_mv = 3300;

这样:

3300mV 就等于 3.300V
2700mV 就等于 2.700V
4200mV 就等于 4.200V

判断低电量时也很方便:

if(battery_mv < 2700)
{
    low_battery_flag = 1;
}

而不是:

if(battery_voltage < 2.7)
{
    low_battery_flag = 1;
}

在单片机中,整数方式更可靠、更高效。


十、变量是什么?

变量可以理解为:

程序运行过程中可以变化的数据。

例如:

unsigned char brightness = 5;

这里定义了一个变量:

变量名:brightness
类型:unsigned char
初始值:5

它表示当前亮度等级是 5。

后面可以改变它:

brightness = 6;
brightness = 7;
brightness++;
brightness--;

变量就像一个小盒子:

盒子名字:brightness
盒子大小:unsigned char
盒子里面的值:5

以后可以往这个盒子里放新的值。


十一、变量的定义格式

变量定义的一般格式是:

数据类型 变量名 = 初始值;

例如:

unsigned char brightness = 5;
unsigned char work_mode = 0;
unsigned int battery_mv = 3000;
bit low_battery_flag = 0;

也可以先定义,再赋值:

unsigned char brightness;

brightness = 5;

但是在嵌入式项目中,建议尽量初始化:

unsigned char brightness = 5;

这样程序刚开始运行时,变量状态更明确。


十二、变量命名建议

变量名不要随便写成:

a
b
c
x
y
z

除非只是临时循环变量。

更推荐使用有意义的名字:

brightness
work_mode
key_value
battery_mv
low_battery_flag
timer_count

例如下面这段代码:

unsigned char a = 5;
unsigned char b = 1;

很难看出 ab 分别代表什么。

如果写成:

unsigned char brightness = 5;
unsigned char work_mode = MODE_NORMAL;

就非常清楚。

好的变量命名可以显著提高代码可读性。


十三、全局变量和局部变量

1. 全局变量

定义在函数外面的变量叫全局变量。

例如:

unsigned char brightness = 5;
bit low_battery_flag = 0;

void main(void)
{
    while(1)
    {
        
    }
}

这里的:

brightness
low_battery_flag

就是全局变量。

全局变量的特点是:

可以被多个函数使用
程序运行期间一直存在
适合保存系统状态

例如下面这些系统状态适合用全局变量:

当前亮度等级
当前工作模式
低电量标志
开关机状态

2. 局部变量

定义在函数内部的变量叫局部变量。

例如:

void Delay_ms(unsigned int ms)
{
    unsigned int i;
    unsigned int j;
}

这里的:

i
j

就是局部变量。

局部变量的特点是:

只在当前函数里面有效
函数执行结束后一般不再使用
适合临时计算

3. 示例对比

unsigned char brightness = 5;    // 全局变量

void Set_Brightness(unsigned char level)
{
    unsigned char pwm_value;     // 局部变量

    pwm_value = level * 10;
}

这里:

brightness 是系统状态,需要长期保存,所以适合做全局变量;
pwm_value 只是函数内部临时计算,所以适合做局部变量。

十四、常量是什么?

常量就是程序运行过程中不希望改变的值。

例如:

#define BRIGHTNESS_MAX_NORMAL     20
#define BRIGHTNESS_MAX_LOW_POWER  10
#define MODE_NORMAL               0
#define MODE_LOW_POWER            1

这些值在程序运行过程中不应该被修改。


1. 为什么要用宏定义常量?

不推荐这样写:

if(brightness < 20)
{
    brightness++;
}

因为以后别人看代码时,可能不知道 20 是什么意思。

更推荐这样写:

#define BRIGHTNESS_MAX_NORMAL    20

if(brightness < BRIGHTNESS_MAX_NORMAL)
{
    brightness++;
}

这样一看就知道:

20 表示普通模式下的最大亮度等级。

2. 常用宏定义示例

#define MODE_NORMAL            0
#define MODE_LOW_POWER         1

#define KEY_NONE               0
#define KEY_UP                 1
#define KEY_DOWN               2
#define KEY_MODE               3

#define BRIGHTNESS_MAX_NORMAL  20
#define BRIGHTNESS_MAX_LOW     10

#define BATTERY_LOW_MV         2700

这些宏定义可以让代码更清楚。

例如:

if(work_mode == MODE_NORMAL)
{
    brightness_max = BRIGHTNESS_MAX_NORMAL;
}
else if(work_mode == MODE_LOW_POWER)
{
    brightness_max = BRIGHTNESS_MAX_LOW;
}

比下面这种代码更容易维护:

if(work_mode == 0)
{
    brightness_max = 20;
}
else if(work_mode == 1)
{
    brightness_max = 10;
}

十五、数据溢出:今天最重要的难点

1. 什么是溢出?

溢出就是:

变量装不下这个数了,结果发生回绕。

例如:

unsigned char a = 255;

a = a + 1;

unsigned char 最大只能表示 255。

那么 255 再加 1 会变成多少?

结果不是 256,而是:

0

这就叫溢出。


2. 通俗理解

unsigned char 就像一个只能显示 0 到 255 的计数器。

当它显示 255 时,再加 1,就会回到 0。

类似汽车里程表:

99999 再加 1,回到 00000

所以:

unsigned char a = 255;
a++;

结果是:

a = 0

3. 亮度变量的溢出问题

假设亮度等级是 0 到 20。

如果写:

unsigned char brightness = 20;

brightness++;

结果会变成 21。

虽然没有超过 unsigned char 的最大范围,但已经超过了业务范围。

所以正确写法应该是:

if(brightness < BRIGHTNESS_MAX_NORMAL)
{
    brightness++;
}

这样亮度最大只会到 20。

同理,降低亮度时不要直接写:

brightness--;

如果 brightness 已经是 0,再减 1:

unsigned char brightness = 0;
brightness--;

结果会变成:

255

这也是溢出。

所以正确写法是:

if(brightness > 0)
{
    brightness--;
}

4. 这个问题非常常见

例如:

unsigned char brightness = 0;

brightness--;

本来以为亮度会保持 0,结果它变成了 255。

如果后面用这个值查亮度表:

pwm_value = brightness_table[brightness];

那就可能访问数组外面的数据,程序可能异常。

所以以后做按键调节亮度时,一定要写边界保护。


十六、整数除法:结果会丢掉小数

在 C 语言里,如果两个整数相除,结果还是整数。

例如:

unsigned int a = 5;
unsigned int b = 2;
unsigned int c;

c = a / b;

结果是:

c = 2

不是 2.5。

因为 c 是整数,小数部分会被直接舍掉。


1. ADC电压计算示例

假设:

adc_value = 2048;

参考电压是 3300mV,12 位 ADC 最大值是 4095。

电压计算:

battery_mv = adc_value * 3300 / 4095;

这里结果是整数 mV。

这种写法在嵌入式中很常见。

如果想减少误差,要注意计算顺序。

推荐:

battery_mv = (unsigned long)adc_value * 3300 / 4095;

不推荐:

battery_mv = adc_value / 4095 * 3300;

因为:

adc_value / 4095

大多数情况下会先算成 0。

例如:

2048 / 4095 = 0
0 × 3300 = 0

结果就完全错误。

所以整数运算要记住:

先乘后除,必要时用 long 防止中间溢出。

十七、类型选择规则总结

以后写 51 程序,可以按照下面规则选择类型。

1. 开关标志

例如:

是否开机
是否低电量
是否按键按下
是否开启某个功能

使用:

bit

示例:

bit power_on_flag = 0;
bit low_battery_flag = 0;
bit key_pressed_flag = 0;
bit function_enable = 1;

2. 小范围状态

例如:

工作模式
亮度等级
按键编号
状态机状态

使用:

unsigned char

示例:

unsigned char work_mode = MODE_NORMAL;
unsigned char brightness = 5;
unsigned char key_value = KEY_NONE;
unsigned char system_state = 0;

3. 中等范围计数

例如:

毫秒计数
ADC 值
电池电压 mV
延时时间

使用:

unsigned int

示例:

unsigned int timer_ms = 0;
unsigned int adc_value = 0;
unsigned int battery_mv = 3000;

4. 大范围计算

例如:

ADC 换算中间结果
累计运行时间
较大乘法计算

使用:

unsigned long

示例:

unsigned long temp_calc = 0;

5. 小数

51 单片机中尽量少用 float

如果必须表示小数,优先改成整数单位。

例如:

3.3V 写成 3300mV
1.2A 写成 1200mA
0.5s 写成 500ms

这样程序更高效,也更稳定。


十八、结合常规小项目定义一组变量

假设现在要做一个简单的小型控制项目,功能包括:

LED 指示
按键调节
模式切换
电池电压检测
低电量提示

可以先定义一些变量:

#define MODE_NORMAL            0
#define MODE_LOW_POWER         1

#define BRIGHTNESS_MAX_NORMAL  20
#define BRIGHTNESS_MAX_LOW     10

#define KEY_NONE               0
#define KEY_UP                 1
#define KEY_DOWN               2
#define KEY_MODE               3

#define BATTERY_LOW_MV         2700

unsigned char work_mode = MODE_NORMAL;
unsigned char brightness = 5;
unsigned char key_value = KEY_NONE;
unsigned char brightness_max = BRIGHTNESS_MAX_NORMAL;

unsigned int battery_mv = 3000;
unsigned int adc_value = 0;
unsigned int system_tick_ms = 0;

bit power_on_flag = 0;
bit low_battery_flag = 0;
bit function_enable = 1;

每个变量的含义如下:

变量名 类型 含义
work_mode unsigned char 当前工作模式
brightness unsigned char 当前亮度等级
key_value unsigned char 当前按键值
brightness_max unsigned char 当前模式最大亮度
battery_mv unsigned int 电池电压,单位 mV
adc_value unsigned int ADC 原始值
system_tick_ms unsigned int 系统毫秒计数
power_on_flag bit 是否开机
low_battery_flag bit 是否低电量
function_enable bit 某个功能是否使能

这段代码虽然还没有直接控制硬件,但是已经具备了一个嵌入式小项目的变量框架。


十九、今天的完整练习代码

今天不增加复杂外设,只在第一天 LED 闪烁代码基础上加入变量和宏定义。

假设:

LED 接 P3.7
LED 低电平点亮

代码如下:

#include "STC8G.H"

#define LED_ON_LEVEL     0
#define LED_OFF_LEVEL    1

#define MODE_SLOW        0
#define MODE_FAST        1

sbit LED = P3^7;

unsigned char led_mode = MODE_SLOW;
unsigned int delay_time_ms = 500;
bit led_state = 0;

void Delay_ms(unsigned int ms)
{
    unsigned int i;
    unsigned int j;

    for(i = 0; i < ms; i++)
    {
        for(j = 0; j < 1000; j++)
        {
            ;
        }
    }
}

void LED_On(void)
{
    LED = LED_ON_LEVEL;
    led_state = 1;
}

void LED_Off(void)
{
    LED = LED_OFF_LEVEL;
    led_state = 0;
}

void main(void)
{
    while(1)
    {
        if(led_mode == MODE_SLOW)
        {
            delay_time_ms = 500;
        }
        else if(led_mode == MODE_FAST)
        {
            delay_time_ms = 100;
        }

        LED_On();
        Delay_ms(delay_time_ms);

        LED_Off();
        Delay_ms(delay_time_ms);
    }
}

这段代码目前是慢闪,因为:

unsigned char led_mode = MODE_SLOW;

如果改成:

unsigned char led_mode = MODE_FAST;

LED 就会快闪。


二十、逐行理解练习代码

1. LED电平宏定义

#define LED_ON_LEVEL     0
#define LED_OFF_LEVEL    1

表示:

LED 点亮时输出 0;
LED 熄灭时输出 1。

如果硬件是高电平点亮,只需要改成:

#define LED_ON_LEVEL     1
#define LED_OFF_LEVEL    0

后面 LED_On()LED_Off() 函数不用改。

这就是宏定义的好处。


2. 闪烁模式宏定义

#define MODE_SLOW        0
#define MODE_FAST        1

表示:

0:慢闪模式
1:快闪模式

这样代码更容易读。


3. 模式变量

unsigned char led_mode = MODE_SLOW;

表示当前 LED 闪烁模式是慢闪。

如果写成:

unsigned char led_mode = MODE_FAST;

表示当前 LED 闪烁模式是快闪。


4. 延时时间变量

unsigned int delay_time_ms = 500;

表示当前延时时间是 500ms。

这里不能用 unsigned char,因为 500 超过了 255。

所以用 unsigned int 更合适。


5. LED状态标志

bit led_state = 0;

表示当前 LED 状态。

0:灭
1:亮

这种只有两个状态的变量,适合用 bit


二十一、亮度变量示例

下面这个例子不一定控制真实 PWM,只用于理解变量范围和边界保护。

#define BRIGHTNESS_MIN    0
#define BRIGHTNESS_MAX    20

unsigned char brightness = 5;

void Brightness_Up(void)
{
    if(brightness < BRIGHTNESS_MAX)
    {
        brightness++;
    }
}

void Brightness_Down(void)
{
    if(brightness > BRIGHTNESS_MIN)
    {
        brightness--;
    }
}

这段代码非常重要。

以后做按键调节参数时,会经常用到类似逻辑。


1. 为什么不能直接写 brightness++?

假设最大亮度是 20。

如果当前已经是 20,再执行:

brightness++;

就会变成 21,超出业务范围。

所以要写:

if(brightness < BRIGHTNESS_MAX)
{
    brightness++;
}

2. 为什么不能直接写 brightness–?

假设当前亮度是 0。

如果执行:

brightness--;

因为 brightnessunsigned char,它会从 0 变成 255。

这就是典型溢出。

所以要写:

if(brightness > BRIGHTNESS_MIN)
{
    brightness--;
}

二十二、今天必须掌握的8个重点

1. unsigned char

适合:

亮度等级
模式编号
按键编号
状态编号
小范围计数

范围:

0 ~ 255

2. unsigned int

适合:

延时时间
毫秒计数
ADC 值
电池电压 mV

范围:

0 ~ 65535

3. bit

适合:

开关标志
真假状态
是否按下
是否低电量

范围:

0 或 1

4. float

51 单片机里尽量少用。

电压、电流、时间尽量用整数单位表示:

3.3V → 3300mV
1.2A → 1200mA
0.5s → 500ms

5. 变量要初始化

推荐:

unsigned char brightness = 5;

不推荐初学阶段写:

unsigned char brightness;

因为后者初始值不明确,容易出问题。


6. 不要滥用数字

不推荐:

if(work_mode == 1)

推荐:

#define MODE_LOW_POWER 1

if(work_mode == MODE_LOW_POWER)

这样代码更容易维护。


7. 防止溢出

不推荐:

brightness--;

推荐:

if(brightness > 0)
{
    brightness--;
}

8. 计算时注意中间结果

不推荐:

battery_mv = adc_value * 3300 / 4095;

在某些情况下,中间乘法可能溢出。

更推荐:

battery_mv = (unsigned long)adc_value * 3300 / 4095;

二十三、今天的小测验

问题1:亮度等级范围是0到20,用什么类型合适?

答案:

unsigned char

问题2:电池电压3300mV,用什么类型合适?

答案:

unsigned int

问题3:低电量标志只有0和1,用什么类型合适?

答案:

bit

问题4:下面代码有什么问题?

unsigned char brightness = 0;

brightness--;

答案:

brightness 会从 0 溢出变成 255。

问题5:下面代码为什么可能有问题?

unsigned int adc_value = 4095;
unsigned int battery_mv;

battery_mv = adc_value * 3300 / 4095;

答案:

adc_value * 3300 的中间结果可能超过 unsigned int 范围,导致溢出。

更安全写法:

battery_mv = (unsigned long)adc_value * 3300 / 4095;

二十四、今天的练习任务

任务1:定义变量

请根据下面含义定义变量:

当前亮度等级,初始值为 5
当前工作模式,初始为普通模式
低电量标志,初始为 0
电池电压,初始为 3000mV
按键值,初始为无按键

参考答案:

#define MODE_NORMAL   0
#define KEY_NONE      0

unsigned char brightness = 5;
unsigned char work_mode = MODE_NORMAL;
bit low_battery_flag = 0;
unsigned int battery_mv = 3000;
unsigned char key_value = KEY_NONE;

任务2:写亮度增加函数

要求:

亮度最大为 20
没有超过 20 时才允许增加

参考答案:

#define BRIGHTNESS_MAX    20

unsigned char brightness = 5;

void Brightness_Up(void)
{
    if(brightness < BRIGHTNESS_MAX)
    {
        brightness++;
    }
}

任务3:写亮度降低函数

要求:

亮度最小为 0
大于 0 时才允许降低

参考答案:

#define BRIGHTNESS_MIN    0

unsigned char brightness = 5;

void Brightness_Down(void)
{
    if(brightness > BRIGHTNESS_MIN)
    {
        brightness--;
    }
}

任务4:写模式切换函数

要求:

当前是普通模式时,切换到低功耗模式
当前是低功耗模式时,切换到普通模式

参考答案:

#define MODE_NORMAL      0
#define MODE_LOW_POWER   1

unsigned char work_mode = MODE_NORMAL;

void Mode_Switch(void)
{
    if(work_mode == MODE_NORMAL)
    {
        work_mode = MODE_LOW_POWER;
    }
    else
    {
        work_mode = MODE_NORMAL;
    }
}

任务5:写低电量判断函数

要求:

电池电压低于 2700mV 时,低电量标志置 1
否则低电量标志清 0

参考答案:

#define BATTERY_LOW_MV    2700

unsigned int battery_mv = 3000;
bit low_battery_flag = 0;

void Battery_Check(void)
{
    if(battery_mv < BATTERY_LOW_MV)
    {
        low_battery_flag = 1;
    }
    else
    {
        low_battery_flag = 0;
    }
}

二十五、第二天完整示例程序

下面把今天的变量、宏定义、函数整合到一个示例里。

这个程序暂时不做真实按键和 ADC,只是演示数据类型和变量管理。

#include "STC8G.H"

#define LED_ON_LEVEL          0
#define LED_OFF_LEVEL         1

#define MODE_NORMAL           0
#define MODE_LOW_POWER        1

#define BRIGHTNESS_MIN        0
#define BRIGHTNESS_MAX_NORMAL 20
#define BRIGHTNESS_MAX_LOW    10

#define BATTERY_LOW_MV        2700

sbit LED = P3^7;

unsigned char work_mode = MODE_NORMAL;
unsigned char brightness = 5;
unsigned char brightness_max = BRIGHTNESS_MAX_NORMAL;

unsigned int battery_mv = 3000;
unsigned int delay_time_ms = 500;

bit low_battery_flag = 0;
bit led_state = 0;

void Delay_ms(unsigned int ms)
{
    unsigned int i;
    unsigned int j;

    for(i = 0; i < ms; i++)
    {
        for(j = 0; j < 1000; j++)
        {
            ;
        }
    }
}

void LED_On(void)
{
    LED = LED_ON_LEVEL;
    led_state = 1;
}

void LED_Off(void)
{
    LED = LED_OFF_LEVEL;
    led_state = 0;
}

void Mode_Switch(void)
{
    if(work_mode == MODE_NORMAL)
    {
        work_mode = MODE_LOW_POWER;
        brightness_max = BRIGHTNESS_MAX_LOW;

        if(brightness > brightness_max)
        {
            brightness = brightness_max;
        }
    }
    else
    {
        work_mode = MODE_NORMAL;
        brightness_max = BRIGHTNESS_MAX_NORMAL;
    }
}

void Brightness_Up(void)
{
    if(brightness < brightness_max)
    {
        brightness++;
    }
}

void Brightness_Down(void)
{
    if(brightness > BRIGHTNESS_MIN)
    {
        brightness--;
    }
}

void Battery_Check(void)
{
    if(battery_mv < BATTERY_LOW_MV)
    {
        low_battery_flag = 1;
    }
    else
    {
        low_battery_flag = 0;
    }
}

void main(void)
{
    while(1)
    {
        Battery_Check();

        if(low_battery_flag == 1)
        {
            delay_time_ms = 100;
        }
        else
        {
            delay_time_ms = 500;
        }

        LED_On();
        Delay_ms(delay_time_ms);

        LED_Off();
        Delay_ms(delay_time_ms);
    }
}

这个程序的逻辑是:

如果电池电压低于 2700mV,LED 快闪;
如果电池电压正常,LED 慢闪。

现在代码中:

unsigned int battery_mv = 3000;

所以 LED 慢闪。

如果改成:

unsigned int battery_mv = 2600;

那么 LED 会快闪。

这就是变量参与程序控制的基本方式。


二十六、第二天总结

今天学习的是 C51 中最常用的数据类型和变量。

可以这样总结:

bit 用来表示开关状态;
unsigned char 用来表示小范围状态和等级;
unsigned int 用来表示时间、计数、电压、ADC;
unsigned long 用来处理较大的中间计算;
float 在 51 单片机里尽量少用。

今天最重要的嵌入式编程习惯是:

变量类型够用就好;
变量要有明确初始值;
小数尽量用整数单位表示;
亮度、模式、按键值要用宏定义增强可读性;
加减变量时要做边界保护;
涉及乘法计算时要注意中间结果是否溢出。

第二天达到下面程度就算合格:

知道 unsigned char、unsigned int、bit 的用途;
能为亮度、模式、按键、电压选择合适类型;
知道 unsigned char 溢出问题;
知道亮度增加和降低要做边界判断;
知道电池电压建议用 mV 整数表示;
能写简单的模式、亮度、电池判断变量逻辑。

后续可以继续学习 C51 的关键语法,例如 sfrsbit、寄存器和位操作。这部分会直接关系到如何控制 P0、P1、P2、P3 等 IO 口。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐