代码优化

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-08-29 08:09:36 Views: 0 Likes: 0 Comments: 0

035.3.3 代码优化

#include "reg52.h"
#include <intrins.h>

sbit LED1 = P1^0;
sbit LED2 = P1^1;
sbit LED3 = P1^2;
sbit LED4 = P1^3;

sbit KEY1 = P3^2;
sbit KEY2 = P3^3;

sbit SUN = P3^7;

typedef unsigned char uchar;
typedef unsigned int uint;

//void Delay20ms(void) // @11.0592MHz
//{
//    unsigned char data i, j;

//    i = 36;
//    j = 217;
//    do
//    {
//        while (--j);
//    } while (--i);
//}

void Delay_xms(uint xms) // @11.0592MHz
{
    uchar data i, j;

    while (xms)
    {
        _nop_();
        i = 2;
        j = 199;
        do
        {
            while (--j);
        } while (--i);
        xms--;
    }
}

void main()
{
    while (1)
    {
        //Delay_xms(1000);
        if (0 == KEY1)
        {
            Delay_xms(20);
            while (0 == KEY1); // while(1);松手检查
            Delay_xms(20);
            LED1 = ~LED1;
        }

        if (0 == KEY2)
        {
            Delay_xms(20);
            while (0 == KEY2); // while(1);松手检查
            Delay_xms(20);
            LED2 = ~LED2;
        }

        if (SUN == 0)
        {
            LED3 = 1;
        }
        else
        {
            LED3 = 0;
        }
    }
}

一、代码优化

本节由 3.2输入检测 工程复制出 3.3输入检测_代码优化 工程,在不改变按键和光敏检测功能的前提下,提高延时函数的复用性、条件判断的安全性以及数据类型的可读性。

代码仍对应以下硬件连接:

输入端                                  输出端
SW3 按下接地 ──► P3.2 / KEY1 ───────► P1.0 / LED1
SW4 按下接地 ──► P3.3 / KEY2 ───────► P1.1 / LED2
光敏模块 DO ───► P3.7 / SUN  ───────► P1.2 / LED3

按键:按下为 0,松开为 1
LED :P1.x 输出 0 时点亮,输出 1 时熄灭

按键和 LED 都是低电平有效,但前者是因为开关闭合后把端口接到 GND,后者是因为 LED 从 +5V 经限流电阻接到单片机端口,端口输出 0 时形成吸电流回路。代码优化不能脱离这些电路事实,否则即使语法正确,也可能得到相反的灯光现象。

1.1 优化延时函数

固定的 Delay20ms() 只能延时约 20 ms。若还需要 30 ms、100 ms 或 1000 ms,就要创建多个高度重复的函数。更通用的办法是先生成约 1 ms 的基础延时代码,再用参数决定重复次数:

flowchart LR
    A[调用 Delay_xms 20] --> B[xms 初值为 20]
    B --> C{是否大于 0}
    C -- 是 --> D[执行一次约 1 ms 的延时代码]
    D --> E[xms 减 1]
    E --> C
    C -- 否 --> F[函数返回]

核心结构是:

void Delay_xms(unsigned int xms)
{
    while (xms)
    {
        /* 约 1 ms 的基础延时代码 */
        xms--;
    }
}

当调用 Delay_xms(20); 时,形参 xms 得到初值 20。循环每执行一次,完成约 1 ms 忙等待并将 xms 减 1;减到 0 后,while(0) 为假,函数退出。Delay_xms(30)Delay_xms(1000) 的原理完全相同。

调用方式基础延时执行次数名义延时
Delay_xms(0)00 ms
Delay_xms(20)20约 20 ms
Delay_xms(30)30约 30 ms
Delay_xms(1000)1000约 1 s

这里要注意五个细节:

  1. 参数是值传递:函数内部递减的是局部形参 xms,不会改变调用处写下的常量,也不会影响其他变量。
  2. 函数调用后要写分号:正确形式是 Delay_xms(20);
  3. _nop_() 需要声明:该内建函数由 Keil C51 的 <intrins.h> 声明,因此源码必须包含该头文件。
  4. 旧函数不再参与编译Delay20ms() 已整体注释。若同时保留定义却不调用,Keil 链接器可能给出 UNCALLED SEGMENT, IGNORED FOR OVERLAY PROCESS 警告,表示该函数对应的代码段没有调用者,通常会被忽略。警告不一定阻止生成 HEX,但仍应理解原因并清理无用代码。
  5. 这是忙等待:延时期间 CPU 只在执行空循环,不能处理其他普通任务。后续需要并行处理显示、串口和多个输入时,应使用定时器或基于系统节拍的非阻塞逻辑。

1 ms 基础代码由 11.0592 MHz、STC89 对应的指令时序生成。把原本独立的 1 ms 函数体嵌入外层循环后,还会增加 while (xms)xms-- 和循环跳转等开销,所以 x ms 是近似值而不是高精度计时。晶振频率、1T/6T/12T 模式、编译器版本和优化级别变化后,都应重新测量或生成延时。

完成构建后,应从 3.3输入检测_代码优化/Objects 目录选择当前生成的 HEX,并结合路径与时间戳核对,避免烧录到其他工程的产物。KEY1 和 KEY2 分别调用新延时函数后,仍应保持一次完整按压只翻转一次对应 LED。

1.2 优化判断语句

判断相等使用 ==,赋值使用 =

if (KEY1 == 0)  /* 判断 KEY1 是否等于 0 */
if (KEY1 = 0)   /* 把 0 写入 KEY1,再判断赋值表达式的结果 */

第二行在 C 语言中是合法语法。它会把 KEY1 对应的 P3.2 端口位写成 0,赋值表达式本身的结果也是 0,因此条件为假。对于 sbit,这不只是普通变量写错,还可能直接改变硬件引脚输出状态。

把常量放在左侧可以让漏写等号更容易在编译期暴露:

if (0 == KEY1)  /* 正常判断 */
if (0 = KEY1)   /* 非法:不能给常量 0 赋值,编译器报错 */

这种写法有时称为“常量在左”或 Yoda condition。KEY1 == 0 本身并不错误,现代编译器通常也可以针对条件中的可疑赋值发出警告;无论采用哪种风格,都要区分 ===,并认真查看 Warning。配套源码把 KEY1、KEY2 写成 0 == KEYx,逻辑与 KEYx == 0 完全相同。

条件判断依旧受电路极性约束:

SW3/SW4 松开:P3.2/P3.3 由内部弱上拉保持为 1
SW3/SW4 按下:端口经按键接 GND,读到 0

因此 if (0 == KEY1) 表示“KEY1 被按下”。进入分支后,程序延时消抖、等待松手、再次延时,然后执行一次 LED1 = ~LED1

光敏模块没有机械弹片,不会产生按键式机械抖动,所以源码直接判断 SUN,不需要等待“松手”。不过,环境光位于电位器阈值附近时,LM393 的 DO 可能因微小光照变化或噪声反复翻转;这属于阈值抖动,可通过调节电位器、连续多次采样、时间滤波或迟滞处理改善。

光敏输入到 LED3 的电平链路为:

光强较强 ─► 光敏电阻阻值下降 ─► LM393 的 DO=0 ─► SUN=0
          ─► 程序写 LED3=1 ─► 低电平有效的 LED3 熄灭

光线较弱 ─► 光敏电阻阻值上升 ─► LM393 的 DO=1 ─► SUN=1
          ─► 程序写 LED3=0 ─► LED3 点亮

1.3 优化int类型

C 语言只规定各整数类型的最小能力和相对大小,具体位宽由编译器和目标平台决定。对本课程使用的 Keil C51 编译环境,常用整数类型如下:

类型位宽字节数取值范围
signed char8 位1 Byte-128~127
unsigned char8 位1 Byte0~255
signed int16 位2 Bytes-32768~32767
unsigned int16 位2 Bytes0~65535
signed long int32 位4 Bytes-2147483648~2147483647
unsigned long int32 位4 Bytes0~4294967295

对于 N 位无符号整数,全部 N 位都表示数值,范围为:

$ 0\sim 2^N-1 $

所以 8 位无符号数最大值为 $2^8-1=255$,16 位无符号数最大值为 $2^{16}-1=65535$。一共仍分别有 $2^8=256$、$2^{16}=65536$ 个不同状态,因为计数从 0 开始。

在常见的二进制补码表示中,N 位有符号整数的范围为:

$ -2^{N-1}\sim 2^{N-1}-1 $

因此 16 位 int 为 -32768~32767。正负范围看起来不完全对称,是因为 0 也占用一个编码,负数一侧多出一个值。

延时参数选择 unsigned int 的原因是:

  • 毫秒数不需要负值,使用无符号类型更符合含义;
  • unsigned char 最大只能保存 255,无法直接表示 Delay_xms(1000) 的 1000;
  • C51 的 unsigned int 可保存 0~65535,足以覆盖本实验的常用毫秒数。

函数内部的 i=2j=199 都没有超过 255,因此使用 unsigned char 可以节省存储空间。在 uchar data i, j; 中,data 还是 Keil C51 的存储区限定符,表示把变量放在内部可直接寻址 DATA 区;它不是变量名,也不是标准 C 数据类型的一部分。

char 不只能保存可见字符。计算机中的字符同样以数值编码保存,例如在常见 ASCII 编码下,字符 'A' 的数值是 65;同一个 8 位类型也可以保存普通小整数或原始字节。charint 的主要区别包括位宽、范围、运算规则和在目标平台上的访问效率,而不是“一个只能存字符、另一个只能存数字”。

在常见 ARM 工具链中,int 通常为 32 位,但这是具体 ABI 和编译器的约定,不能只凭 C 关键字假定所有平台都相同。跨平台代码若必须精确表达位宽,应优先使用 <stdint.h> 中的 uint8_tuint16_tuint32_t 等定宽类型,并通过目标编译器再次确认。

还要注意溢出:Delay_xms 的参数是 16 位,传入超过 65535 的整数时,转换后的值可能不再是期望的毫秒数;即使传入 60000,阻塞约一分钟也会让主程序长时间无法响应按键和光敏输入。长时间计时应改用硬件定时器或系统时基。

1.4 优化类型别名

typedef 可以为已有类型定义更简洁的别名:

typedef unsigned char uchar;
typedef unsigned int uint;

语法顺序是:

typedef + 已有类型 + 新别名 + 分号

typedef unsigned int uint;
        └────原类型───┘ └新名称┘

定义后,下面两组声明完全等价:

unsigned int xms;
uint xms;

unsigned char data i, j;
uchar data i, j;

typedef 不会创建一种具有新位宽的新整数,也不会改变取值范围、内存布局或运行效率;它只是给现有类型增加一个名称。uint 在本工程中仍然就是 Keil C51 的 16 位 unsigned intuchar 仍然是 8 位 unsigned char

类型别名可以减少重复书写,但命名要有统一规则。如果工程或第三方库已经定义了同名 uint,再次定义可能产生冲突;大型或跨平台项目更适合使用含义明确的定宽类型。无论使用原类型名、typedef 别名还是 <stdint.h>,选择类型时都应先确认数值范围、符号需求和目标平台的真实位宽。

本节程序主体仍运行在 while(1) 无限循环中,持续扫描按键和光敏输入。后续引入中断后,中断服务函数会在主循环之外定义,并由硬件事件触发执行;主循环与中断之间共享的数据还需要考虑 volatile、原子访问和并发一致性。