C51 | 页面生命周期的实现

40 阅读11分钟

本文基于江协的代码,以及普中 A2。(虽然江协的代码质量让人一言难尽)

本文面向具有一定知识基础的 C51 初学者。本人也是刚学,喜欢擅自制造概念,若有内容有疏漏请指出。

本文先介绍什么是页面生命周期,再介绍若干种基本的或简单的页面生命周期关系。

转载须注明出处。

1 main() 函数提供入口页面

在主循环前,初始化 LCD1602,就可以在此后通过 LCD1602 来构建文本页面。

#include <REGX52.H>

#include "LCD1602.h"
#include "Delay.h"

unsigned int time_count;

void main
{
    LCD_Init();
    
    // 可以在初始化后调用 LCD1602 的方法
    LCD_ShowString(1, 1, "Hello, World!");
    
    while (1) 
    {
        // 可以在主循环调用 LCD1602 的方法
        LCD_ShowNum(2, 1, time_count % 10, 1);
        
        Delay(1000);
        time_count++;
    }
}

这里所谓的 页面,就是让单片机处于所设计的一个特殊状态,并通过 LCD1602 这个显示屏将状态表达出来。输入的数据可以来自按键,传感器等;输出的数据可以指向蜂鸣器,电动机等。

主要的页面构建方法有两种,一个是在指定位置显示文本,一个是在指定位置显示数字。通过在指定位置显示空字符串,可以实现字符位清除的效果。

2 函数是页面生命周期的一种形式

比如说在主循环里写了这么一个业务,按下 K3 时,LCD1602 上显示 “K3 on”,延时 1000 毫秒后清除这一行字,那么就相当于存在了一个页面。

#include <REGX52.H>

#include "LCD1602.h"
#include "Key.h"

unsigned char key_active;

void main() {
    LCD_Init();
  
    while (1) {
        key_active = Key();
        
        while (key_active == 3) {
            LCD_ShowString(1, 1, "K3 on");
            Delay(1000);
            LCD_ShowString(1, 1, "     ");
        }
    }
}

这里所谓的生命周期,就是一个设计对象可以被提及的范围,包括作用域,状态事项,业务逻辑,接口等。生命周期从对象被单片机的软件环境创建开始,至软件环境将它销毁而结束。举个例子,上述页面被关闭的时候,要记得把 K3 on 这一行字给销毁掉,不要留给别的页面。

页面的生命周期开始于 K3 被按下,结束于延时结束然后那行字被清除。 为了体现这个页面的存在性,以至于方便进一步的构建,可以把相关业务逻辑给提出来,放到一个函数里。

当执行到一个函数的时候,线程被这个函数中断,当页面的生命周期后中断才会被返回。

#include <REGX52.H>

#include "LCD1602.h"
#include "Key.h"

void main() {
    LCD_Init();
    
    while (1) {
        key_active = Key();
    
        while (key_active == 3) {
            // 中断发生
            page();
            // 中断返回
        }
    }
}

page() {
    // page 生命周期开始
    LCD_ShowString("K3 on");
    Delay(1000);
    // page 生命周期结束
    LCD_ShowString("     ");
}

这样子分开,有一个好处。比如说有一个地方要改动,可以很快地找到应该改哪里,而不是在 main() 里面到处找。另一方面,page() 将可以复用,就像 Delay() 这个函数可以复用。

3 页面生命周期的分支构型与注册构型

在上一个例子,构建了一个简单的结构:

graph LR
A[entry] --> B[loop]
B --> C[page]
C --> B

如果结构复杂一点,比如说加了一个新的页面,并且由一个新的按键作为入口:

graph LR
    A[entry] --> B[loop]
    B --> C[page 1]
    C --> B
    B --> D[page 2]
    D --> B

可以这么说,分支构型就是在主循环中通过条件判断来决定进入哪个页面。这是最直观的实现方式:

#include <REGX52.H>
#include "LCD1602.h"
#include "Key.h"

unsigned char key_active;

void page1() {
    // page 1 生命周期开始
    LCD_ShowString(1, 1, "Page 1     ");
    Delay(1000);
    // page 1 生命周期结束
    LCD_ShowString(1, 1, "           ");
}

void page2() {
    LCD_ShowString(1, 1, "Page 2     ");
    Delay(1000);
    LCD_ShowString(1, 1, "           ");
}

void main() {
    LCD_Init();
    
    while (1) {
        key_active = Key();
        
        // 分支判断
        if (key_active == 1) {
            page1();
        } else if (key_active == 2) {
            page2();
        }
        // 此处可以继续写
    }
}

这种方式简单直接,但随着页面增多,main() 函数会变得冗长,形如ξ 这个希腊字母。此时可以考虑注册构型,通过一个中转函数来管理页面跳转:

#include <REGX52.H>
#include "LCD1602.h"
#include "Key.h"

// 页面函数声明
void page1();
void page2();
void page3();

// 页面跳转中转函数
void page_router(unsigned char page_id) {
    switch(page_id) {
        case 1:
            page1();
            break;
        case 2:
            page2();
            break;
        case 3:
            page3();
            break;
        default:
            // 默认页面或错误处理
            break;
    }
}

void main() {
    LCD_Init();
    unsigned char key_active;
    
    while (1) {
        key_active = Key();
        
        // 只有有效按键才触发页面跳转
        if (key_active != 0) {
            page_router(key_active);
        }
    }
}

// 页面函数定义
void page1() {
    LCD_ShowString(1, 1, "Page 1 Active ");
    Delay(800);
    LCD_ShowString(1, 1, "              ");
}

void page2() {
    LCD_ShowString(1, 1, "Page 2 Active ");
    Delay(800);
    LCD_ShowString(1, 1, "              ");
}

void page3() {
    LCD_ShowString(1, 1, "Page 3 Active ");
    Delay(800);
    LCD_ShowString(1, 1, "              ");
}

注册构型的本质是在实现解耦,即所有页面跳转逻辑在一个函数中,而不是和主循环的其它部分混在一起。如果要添加或减去生命周期起点,只需要修改注册函数。

有些人可能会联想到 hook,中断向量表,Hash 谱,注册表,监听器之类的东西。同理。

4 页面生命周期的递归构型与线性构型

4.1 递归构型

递归构型指的是页面可以调用自身或其他页面,形成调用链。这种方式在菜单系统、状态机中常见:

void main_menu() {
    LCD_ShowString(1, 1, "Main Menu     ");
    LCD_ShowString(2, 1, "1.Set 2.View  ");
    
    unsigned char key = 0;
    while (key == 0) {
        key = Key();
    }
    
    if (key == 1) {
        settings_menu();  // 进入设置菜单
    } else if (key == 2) {
        view_data();      // 进入数据查看
    }
    // 返回后重新显示主菜单
    main_menu();  // 递归调用
}

void settings_menu() {
    LCD_ShowString(1, 1, "Settings      ");
    LCD_ShowString(2, 1, "1.Time 2.Back ");
    
    unsigned char key = 0;
    while (key == 0) {
        key = Key();
    }
    
    if (key == 1) {
        set_time();       // 进入时间设置
    }
    // key == 2 时自然返回
}

递归构型容易去设计,符合思维习惯,但容易造成内存方面的诡异行为(此处不作介绍)。

4.2 线性构型

线性构型通过状态变量控制页面流转,避免深层递归。线性构型的设计受到了一定的约束,但设计充分后容易去实现。

这里提供一个更合适的线性构型示例:

// 页面状态定义
#define PAGE_MAIN      0
#define PAGE_SETTINGS  1
#define PAGE_SET_TIME  2
#define PAGE_VIEW_DATA 3

unsigned char current_page = PAGE_MAIN;

void main() {
    LCD_Init();
    
    while (1) {
        unsigned char key = Key();
        
        // 根据当前页面和按键决定下一个页面
        switch(current_page) {
            case PAGE_MAIN:
                if (key == 1) {
                    LCD_ShowString(1, 1, "Settings Page  ");
                    current_page = PAGE_SETTINGS;
                } else if (key == 2) {
                    LCD_ShowString(1, 1, "Data View      ");
                    current_page = PAGE_VIEW_DATA;
                }
                break;
                
            case PAGE_SETTINGS:
                if (key == 1) {
                    LCD_ShowString(1, 1, "Set Time       ");
                    current_page = PAGE_SET_TIME;
                } else if (key == 2) {
                    LCD_ShowString(1, 1, "Main Menu      ");
                    current_page = PAGE_MAIN;
                }
                break;
                
            case PAGE_SET_TIME:
                if (key == 2) {
                    LCD_ShowString(1, 1, "Settings       ");
                    current_page = PAGE_SETTINGS;
                }
                break;
                
            case PAGE_VIEW_DATA:
                if (key == 2) {
                    LCD_ShowString(1, 1, "Main Menu      ");
                    current_page = PAGE_MAIN;
                }
                break;
        }
    }
}

5 页面的语境捕获

页面对象获取数据的形式主要有:

  • 向页面的实现函数传入参数
  • 与别的设计对象共享内存,例如借助 AT24C02 保存密码
  • 与别的设计对象通信,例如借助 DS18B20 读取当前温度
  • 在函数内部指定字面量,例如之前的 "K3 on" 这个字面量。
  • 函数对全局变量的访问,例如 REGX52.H 指定的引脚别名。

下面是一个更合适的语境捕获示例,展示如何通过参数传递和全局变量共享数据:

#include <REGX52.H>
#include "LCD1602.h"
#include "DS18B20.h"

// 全局变量 - 共享数据
int current_temperature = 0;

// 通过参数传递页面配置
void display_page(unsigned char row, unsigned char col, char* message, unsigned int duration) {
    LCD_ShowString(row, col, message);
    Delay(duration);
    // 清理显示
    LCD_ShowString(row, col, "                ");
}

// 使用全局变量获取数据
void temperature_page() {
    char temp_str[16];
    
    // 从DS18B20读取温度
    current_temperature = DS18B20_ReadT();
    
    // 格式化显示
    sprintf(temp_str, "Temp: %d.%dC", current_temperature/10, current_temperature%10);
    display_page(1, 1, temp_str, 2000);
}

// 主函数
void main() {
    LCD_Init();
    DS18B20_Init();
    
    while (1) {
        if (P3_0 == 0) {  // 按键K1按下
            temperature_page();
        }
    }
}

良好的语境设计可以使页面既独立又协同,既能完成特定功能,又能与其他模块有效通信。

6 退化规划

在复杂系统中,有时需要简化页面关系。退化规划的核心思想是:将复杂的页面关系退化为简单关系。注意,过度的关系简化可能会让具体的函数实现变得复杂。

下面是一个更实际的退化规划示例,展示如何将多级菜单简化为扁平结构:

// 原始的多级菜单系统(复杂)
void complex_system() {
    while (1) {
        main_menu();  // 主菜单
        // 主菜单调用二级菜单
        // 二级菜单调用三级菜单
        // 形成深层调用链
    }
}

// 退化后的扁平结构(简化)
void simple_system() {
    unsigned char page_id = 0;  // 当前页面ID
    unsigned char param = 0;    // 页面参数
    
    while (1) {
        unsigned char key = Key();
        
        // 扁平化的页面处理
        switch(page_id) {
            case 0:  // 主页面
                LCD_ShowString(1, 1, "Main:1-3 Select");
                if (key == 1) page_id = 1;
                if (key == 2) page_id = 2;
                if (key == 3) page_id = 3;
                break;
                
            case 1:  // 页面1
                LCD_ShowString(1, 1, "Page 1 Active  ");
                if (key == 4) {
                    param = 10;  // 设置参数
                    page_id = 0; // 返回主页
                }
                break;
                
            case 2:  // 页面2
                LCD_ShowString(1, 1, "Page 2 Active  ");
                if (key == 4) page_id = 0;
                break;
                
            case 3:  // 页面3
                LCD_ShowString(1, 1, "Page 3 Active  ");
                if (key == 4) page_id = 0;
                break;
        }
    }
}

退化规划的注意事项:

  • 目标是降低复杂度,不是提高复杂度
  • 尽量避免多层页面嵌套
  • 所有页面使用相同的调用方式
  • 页面状态由外部管理,页面本身无状态

7 反组合规划

反组合规划指的是,先构建整体框架,再填充结构无关的功能。反组合规划有利于核心架构的早期确定。

下面是一个更适合C51环境的反组合规划示例:

// 第一步:定义核心框架
typedef struct {
    void (*show)(void);      // 显示函数
    void (*handle_key)(unsigned char key);  // 按键处理函数
    unsigned char id;        // 页面ID
} Page;

// 第二步:声明页面数组(最大支持8个页面)
Page pages[8];
unsigned char page_count = 0;
unsigned char current_page = 0;

// 第三步:页面注册函数
void register_page(void (*show)(void), void (*handle_key)(unsigned char), unsigned char id) {
    if (page_count < 8) {
        pages[page_count].show = show;
        pages[page_count].handle_key = handle_key;
        pages[page_count].id = id;
        page_count++;
    }
}

// 第四步:主框架
void main() {
    LCD_Init();
    
    // 初始化框架
    init_framework();
    
    while (1) {
        // 显示当前页面
        if (pages[current_page].show != NULL) {
            pages[current_page].show();
        }
        
        // 处理按键
        unsigned char key = Key();
        if (key != 0 && pages[current_page].handle_key != NULL) {
            pages[current_page].handle_key(key);
        }
        
        Delay(50);  // 简单延时,避免CPU过载
    }
}

// 第五步:最后实现具体页面功能
void home_show() {
    LCD_ShowString(1, 1, "Home Page      ");
    LCD_ShowString(2, 1, "Press K1-K3    ");
}

void home_handle_key(unsigned char key) {
    if (key == 1) {
        current_page = 1;  // 跳转到页面1
    }
}

// 框架初始化
void init_framework() {
    // 注册主页
    register_page(home_show, home_handle_key, 0);
    
    // 这里可以注册更多页面...
    // 每个页面的具体实现可以稍后完成
}

8 实例

环境检测系统

  1. 上电显示输入 8 位密码,密码错误则重新输入,密码正确进入主界面,按下独立按键 K1 确定,按下独立按键 k2 清除
  2. 主界面显示实时温度显示小数点后一位),光照每 2 秒更新一次
  3. 按下独立按键 K1 进入设置界面,菜单选择温度或密码设置 温度界面可设置温度的最大值,超过阈值蜂鸣器报警 密码设置界面可设置 8 位密码,关机重启密码变为重新设置的新密码
  4. 最多设置 3 位独立按键用来进行页面切换矩阵键盘只能用于数字输入
  5. 设置完成后可返回主界面

我不想在这里讨论,怎么去实现。简单阐述一种架构思路吧。

首先,要有 5 个页面:

graph TD
    A[密码输入页面] --> B[主界面页面]
    B --> C[设置菜单页面]
    C --> D[温度设置页面]
    C --> E[密码设置页面]
    D --> B
    E --> B

然后,密码输入和主页面,可以退化到 main() 函数里。使用线性构型管理页面流转:

// 页面定义
#define PAGE_PASSWORD  0  // 密码输入页
#define PAGE_MAIN      1  // 主界面
#define PAGE_SETTINGS  2  // 设置菜单
#define PAGE_SET_TEMP  3  // 温度设置
#define PAGE_SET_PASS  4  // 密码设置

unsigned char current_page = PAGE_PASSWORD;
unsigned char password[8] = {1,2,3,4,5,6,7,8}; // 默认密码

// 主框架
void main() {
    init_all();  // 初始化所有硬件
    
    while (1) {
        switch(current_page) {
            case PAGE_PASSWORD:
                handle_password_page();
                break;
            case PAGE_MAIN:
                handle_main_page();
                break;
            case PAGE_SETTINGS:
                handle_settings_page();
                break;
            case PAGE_SET_TEMP:
                handle_temp_set_page();
                break;
            case PAGE_SET_PASS:
                handle_pass_set_page();
                break;
        }
    }
}

最后再去实现细枝末节,比如密码保存(用AT24C02),高温报警(用蜂鸣器),温度读取(用DS18B20),光照检测(用光敏电阻)等等。


没有绝对的最佳实践,只有最适合当前项目的设计选择。差不多得了。