9种单片机常用的软件架构

长文预警,加代码5000多字,写了4个多小时,盘软件架构,这篇文章就够了!

可能很多工程师,工作了很多年,都不会有软件架构的概念。

因为我在做研发工程师的第6年,才开始意识到这个东西,在此之前,都是做一些比较简单的项目,一个main函数干到底,架构复杂了反而是累赘。

后面有幸,接触了稍微复杂点的项目,感觉以前水平Hold不住,然后借着项目需求,学习了很多优秀的代码架构,比如以前同事的,一些模组厂的SDK,还有市面上成熟的系统。

说出来可能有点夸张,一个好项目带来的成长,顶你做几年小项目。

在一个工程师从入门到成为高级工程师,都会经历哪些软件架构?

下面给大家盘点一下,每个都提供了简易的架构模型代码。

1.线性架构

这是最简单的一种程序设计方法,也就是我们在入门时写的,下面是一个使用C语言编写的线性架构示例:

cpp 复制代码
#include <reg51.h>  // 包含51系列单片机的寄存器定义

// 延时函数,用于产生一定的延迟
void delay(unsigned int count) {
    unsigned int i;
    while(count--) {
        for(i = 0; i < 120; i++) {}  // 空循环,用于产生延迟
    }
}

void main() {
    // 初始设置P1端口为输出模式,用于控制LED
    P1 = 0xFF;  // 将P1端口设置为高电平,关闭所有LED

    while(1) {  // 无限循环
        P1 = 0x00;  // 将P1端口设置为低电平,点亮所有LED
        delay(500000);  // 调用延时函数,延迟一段时间

        P1 = 0xFF;  // 将P1端口设置为高电平,关闭所有LED
        delay(500000);  // 再次调用延时函数,延迟相同的时间
    }
}

2.模块化架构

模块化架构是一种将程序分解为独立模块的设计方法,每个模块执行特定的任务。

这种架构有助于代码的重用、维护和测试。

下面是一个使用C语言编写的模块化架构示例,该程序模拟了一个简单的交通信号灯控制系统。

cpp 复制代码
#include <reg51.h>  // 包含51系列单片机的寄存器定义

// 定义信号灯的状态
typedef enum {
    RED_LIGHT,
    YELLOW_LIGHT,
    GREEN_LIGHT
} TrafficLightState;

// 函数声明
void initializeTrafficLight(void);
void setTrafficLight(TrafficLightState state);
void delay(unsigned int milliseconds);

// 信号灯控制主函数
void main(void) {
    initializeTrafficLight();  // 初始化交通信号灯

    while(1) {
        setTrafficLight(RED_LIGHT);
        delay(5000);  // 红灯亮5秒

        setTrafficLight(YELLOW_LIGHT);
        delay(2000);  // 黄灯亮2秒

        setTrafficLight(GREEN_LIGHT);
        delay(5000);  // 绿灯亮5秒
    }
}

// 初始化交通信号灯的函数
void initializeTrafficLight(void) {
    // 这里可以添加初始化代码,比如设置端口方向、默认状态等
    // 假设P1端口连接了信号灯,初始状态为熄灭(高电平)
    P1 = 0xFF;
}

// 设置交通信号灯状态的函数
void setTrafficLight(TrafficLightState state) {
    switch(state) {
        case RED_LIGHT:
            // 设置红灯亮,其他灯灭
            P1 = 0b11100000;  // 假设低电平有效,这里设置P1.0为低电平,其余为高电平
            break;
        case YELLOW_LIGHT:
            // 设置黄灯亮,其他灯灭
            P1 = 0b11011000;  // 设置P1.1为低电平,其余为高电平
            break;
        case GREEN_LIGHT:
            // 设置绿灯亮,其他灯灭
            P1 = 0b11000111;  // 设置P1.2为低电平,其余为高电平
            break;
        default:
            // 默认为熄灭所有灯
            P1 = 0xFF;
            break;
    }
}

// 延时函数,参数是毫秒数
void delay(unsigned int milliseconds) {
    unsigned int delayCount = 0;
    while(milliseconds--) {
        for(delayCount = 0; delayCount < 120; delayCount++) {
            // 空循环,用于产生延时
        }
    }
}

3.层次化架构

层次化架构是一种将系统分解为多个层次的设计方法,每个层次负责不同的功能。

着以下是一个使用C语言编写的层次化架构示例,模拟了一个具有不同权限级别的嵌入式系统。

cpp 复制代码
#include <reg51.h>  // 包含51系列单片机的寄存器定义

// 定义不同的操作级别
typedef enum {
    LEVEL_USER,
    LEVEL_ADMIN,
    LEVEL_SUPERUSER
} OperationLevel;

// 函数声明
void systemInit(void);
void performOperation(OperationLevel level);
void displayMessage(char* message);

// 系统初始化后的主循环
void main(void) {
    systemInit();  // 系统初始化

    // 模拟用户操作
    performOperation(LEVEL_USER);
    // 模拟管理员操作
    performOperation(LEVEL_ADMIN);
    // 模拟超级用户操作
    performOperation(LEVEL_SUPERUSER);

    while(1) {
        // 主循环可以是空闲循环或者处理其他低优先级任务
    }
}

// 系统初始化函数
void systemInit(void) {
    // 初始化系统资源,如设置端口、中断等
    // 这里省略具体的初始化代码
}

// 执行不同级别操作的函数
void performOperation(OperationLevel level) {
    switch(level) {
        case LEVEL_USER:
          //用户操作具体代码
            break;
        case LEVEL_ADMIN:
          //管理员操作具体代码
            break;
        case LEVEL_SUPERUSER:
           //超级用户操作具体代码
            break;
    }
}

// 显示消息的函数
void displayMessage(char* message) {
    // 这里省略了实际的显示代码,因为单片机通常没有直接的屏幕输出
    // 消息可以通过LED闪烁、串口输出或其他方式展示
    // 假设通过P1端口的LED展示,每个字符对应一个LED闪烁模式
    // 实际应用中,需要根据硬件设计来实现消息的显示
}

4. 事件驱动架构

事件驱动架构是一种编程范式,其中程序的执行流程由事件(如用户输入、传感器变化、定时器到期等)触发。

在单片机开发中,事件驱动架构通常用于响应外部硬件中断或软件中断。

以下是一个使用C语言编写的事件驱动架构示例,模拟了一个基于按键输入的LED控制。

cpp 复制代码
#include <reg51.h>  // 包含51系列单片机的寄存器定义

// 定义按键和LED的状态
#define KEY_PORT P3  // 假设按键连接在P3端口
#define LED_PORT P2  // 假设LED连接在P2端口

// 函数声明
void delay(unsigned int milliseconds);
bit checkKeyPress(void);  // 返回按键是否被按下的状态(1表示按下,0表示未按下)

// 定时器初始化函数
void timer0Init(void) 
{
    TMOD = 0x01;  // 设置定时器模式寄存器,使用模式1(16位定时器)
    TH0 = 0xFC;   // 设置定时器初值,用于产生定时中断
    TL0 = 0x18;
    ET0 = 1;      // 开启定时器0中断
    EA = 1;       // 开启总中断
    TR0 = 1;      // 启动定时器
}

// 定时器中断服务程序
void timer0_ISR() interrupt 1 
{
    // 定时器溢出后自动重新加载初值,无需手动重置
    // 这里可以放置定时器溢出后需要执行的代码
}

// 按键中断服务程序
bit keyPress_ISR(void) interrupt 2 using 1 
{
    if(KEY_PORT != 0xFF) // 检测是否有按键按下
        {  
        LED_PORT = ~LED_PORT;  // 如果有按键按下,切换LED状态
        delay(20);  // 去抖动延时
        while(KEY_PORT != 0xFF);  // 等待按键释放
        return 1;  // 返回按键已按下
    }
    return 0;  // 如果没有按键按下,返回0
}

// 延时函数,参数是毫秒数
void delay(unsigned int milliseconds) {
    unsigned int i, j;
    for(i = 0; i < milliseconds; i++)
        for(j = 0; j < 1200; j++);  // 空循环,用于产生延时
}

// 主函数
void main(void) 
{
    timer0Init();  // 初始化定时器
    LED_PORT = 0xFF;  // 初始LED熄灭(假设低电平点亮LED)

    while(1) 
    {
        if(checkKeyPress())
        {  // 检查是否有按键按下事件
            // 如果有按键按下,这里可以添加额外的处理代码
        }
    }
}

// 检查按键是否被按下的函数
bit checkKeyPress(void) 
{
    bit keyState = 0;
    // 模拟按键中断触发,实际应用中需要连接硬件中断
    if(1) // 假设按键中断触发
    {  
      keyState = keyPress_ISR();  // 调用按键中断服务程序
    }
    return keyState;  // 返回按键状态
}

事实上,真正的事件型驱动架构,是非常复杂的,我职业生涯的巅峰之作,就是用的事件型驱动架构。

5.状态机架构

在单片机开发中,状态机常用于处理复杂的逻辑和事件序列,如用户界面管理、协议解析等。

以下是一个使用C语言编写的有限状态机(FSM)的示例,模拟了一个简单的自动售货机的状态转换。

cpp 复制代码
#include <reg51.h>  // 包含51系列单片机的寄存器定义

// 定义自动售货机的状态
typedef enum {
    IDLE,
    COIN_INSERTED,
    PRODUCT_SELECTED,
    DISPENSE,
    CHANGE_RETURNED
} VendingMachineState;

// 定义事件
typedef enum {
    COIN_EVENT,
    PRODUCT_EVENT,
    DISPENSE_EVENT,
    REFUND_EVENT
} VendingMachineEvent;

// 函数声明
void processEvent(VendingMachineEvent event);
void dispenseProduct(void);
void returnChange(void);

// 当前状态
VendingMachineState currentState = IDLE;

// 主函数
void main(void)
{
    // 初始化代码(如果有)
    // ...

    while(1)
    {
        // 假设事件由外部触发,这里使用一个模拟事件
        VendingMachineEvent currentEvent = COIN_EVENT; // 模拟投入硬币事件

        processEvent(currentEvent);  // 处理当前事件
    }
}

// 处理事件的函数
void processEvent(VendingMachineEvent event)
{
    switch(currentState)
    {
        case IDLE:
            if(event == COIN_EVENT)
            {
                // 如果在空闲状态且检测到硬币投入事件,则转换到硬币投入状态
                currentState = COIN_INSERTED;
            }
            break;
        case COIN_INSERTED:
            if(event == PRODUCT_EVENT)
            {
                // 如果在硬币投入状态且用户选择商品,则请求出货
                currentState = PRODUCT_SELECTED;
            }
            break;
        case PRODUCT_SELECTED:
            if(event == DISPENSE_EVENT)
            {
                dispenseProduct();  // 出货商品
                currentState = DISPENSE;
            }
            break;
        case DISPENSE:
            if(event == REFUND_EVENT)
            {
                returnChange();  // 返回找零
                currentState = CHANGE_RETURNED;
            }
            break;
        case CHANGE_RETURNED:
            // 等待下一个循环,返回到IDLE状态
            currentState = IDLE;
            break;
        default:
            // 如果状态非法,重置为IDLE状态
            currentState = IDLE;
            break;
    }
}

// 出货商品的函数
void dispenseProduct(void)
{
    // 这里添加出货逻辑,例如激活电机推出商品
    // 假设P1端口连接了出货电机
    P1 = 0x00;  // 激活电机
    // ... 出货逻辑
    P1 = 0xFF;  // 关闭电机
}

// 返回找零的函数
void returnChange(void)
{
    // 这里添加找零逻辑,例如激活机械臂放置零钱
    // 假设P2端口连接了找零机械臂
    P2 = 0x00;  // 激活机械臂
    // ... 找零逻辑
    P2 = 0xFF;  // 关闭机械臂
}

6.面向对象架构

STM32的库,就是一种面向对象的架构。

不过在单片机由于资源限制,OOP并不像在高级语言中那样常见,但是一些基本概念如封装和抽象仍然可以被应用。

虽然C语言本身并不直接支持面向对象编程,但可以通过结构体和函数指针模拟一些面向对象的特性。

下面是一个简化的示例,展示如何在C语言中模拟面向对象的编程风格,以51单片机为背景,创建一个简单的LED类。

cpp 复制代码
#include <reg51.h>

// 定义一个LED类
typedef struct {
    unsigned char state;  // LED的状态
    unsigned char pin;    // LED连接的引脚
    void (*turnOn)(struct LED*);  // 点亮LED的方法
    void (*turnOff)(struct LED*); // 熄灭LED的方法
} LED;

// LED类的构造函数
void LED_Init(LED* led, unsigned char pin) {
    led->state = 0;  // 默认状态为熄灭
    led->pin = pin;   // 设置LED连接的引脚
}

// 点亮LED的方法
void LED_TurnOn(LED* led) {
    // 根据引脚状态点亮LED
    if(led->pin < 8) {
        P0 |= (1 << led->pin);  // 假设P0.0到P0.7连接了8个LED
    } else {
        P1 &= ~(1 << (led->pin - 8));  // 假设P1.0到P1.7连接了另外8个LED
    }
    led->state = 1;  // 更新状态为点亮
}

// 熄灭LED的方法
void LED_TurnOff(LED* led) {
    // 根据引脚状态熄灭LED
    if(led->pin < 8) {
        P0 &= ~(1 << led->pin);  // 熄灭P0上的LED
    } else {
        P1 |= (1 << (led->pin - 8));  // 熄灭P1上的LED
    }
    led->state = 0;  // 更新状态为熄灭
}

// 主函数
void main(void) {
    LED myLed;  // 创建一个LED对象
    LED_Init(&myLed, 3);  // 初始化LED对象,连接在P0.3

    // 给LED对象绑定方法
    myLed.turnOn = LED_TurnOn;
    myLed.turnOff = LED_TurnOff;

    // 使用面向对象的风格控制LED
    while(1) {
        myLed.turnOn(&myLed);  // 点亮LED
        // 延时
        myLed.turnOff(&myLed); // 熄灭LED
        // 延时
    }
}

这段代码定义了一个结构体LED,模拟面向对象中的"类。

这个示例仅用于展示如何在C语言中模拟面向对象的风格,并没有使用真正的面向对象编程语言的特性,如继承和多态,不过对于单片机的应用,足以。

7.基于任务的架构

这种我最喜欢用,结构,逻辑清晰,每个任务都能灵活调度。

基于任务的架构是将程序分解为独立的任务,每个任务执行特定的工作。

在单片机开发中,如果没有使用实时操作系统,我们可以通过编写一个简单的轮询调度器来模拟基于任务的架构。

以下是一个使用C语言编写的基于任务的架构的示例,该程序在51单片机上实现。

为了简化,我们将使用一个简单的轮询调度器来在两个任务之间切换:一个是按键扫描任务,另一个是LED闪烁任务。

cpp 复制代码
#include <reg51.h>

// 假设P1.0是LED输出
sbit LED = P1^0;

// 全局变量,用于记录系统Tick
unsigned int systemTick = 0;

// 任务函数声明
void taskLEDBlink(void);
void taskKeyScan(void);

// 定时器0中断服务程序,用于产生Tick
void timer0_ISR() interrupt 1 using 1 
{
    // 定时器溢出后自动重新加载初值,无需手动重置
    systemTick++;  // 更新系统Tick计数器
}

// 任务调度器,主函数中调用,负责任务轮询
void taskScheduler(void) 
{
    // 检查系统Tick,决定是否执行任务
    // 例如,如果我们需要每1000个Tick执行一次LED闪烁任务
    if (systemTick % 1000 == 0) 
    {
       taskLEDBlink();
    }
    // 如果有按键任务,可以类似地检查Tick并执行
    if (systemTick % 10 == 0) 
    {
       taskKeyScan();
    }
}

// LED闪烁任务
void taskLEDBlink(void) 
{
    static bit ledState = 0;  // 用于记录LED的当前状态
    ledState = !ledState;  // 切换LED状态
    LED = ledState;         // 更新LED硬件状态
}

// 按键扫描任务(示例中省略具体实现)
void taskKeyScan(void) 
{
    // 按键扫描逻辑
}

// 主函数
void main(void) 
{
    // 初始化LED状态
    LED = 0;

    // 定时器0初始化设置
    TMOD &= 0xF0;  // 设置定时器模式寄存器,使用模式1(16位定时器/计数器)
    TH0 = 0x4C;     // 设置定时器初值,产生定时中断(定时周期取决于系统时钟频率)
    TL0 = 0x00;
    ET0 = 1;        // 允许定时器0中断
    EA = 1;         // 允许中断
    TR0 = 1;        // 启动定时器0

    while(1) 
    {
        taskScheduler();  // 调用任务调度器
    }
}

这里只是举个简单的例子,这个代码示例,比较适合51和stm8这种资源非常少的单片机。

8.代理架构

这个大家或许比较少听到过,但在稍微复杂的项目中,是非常常用的。

在代理架构中,每个代理(Agent)都是一个独立的实体,它封装了特定的决策逻辑和数据,并与其他代理进行交互。

在实际项目中,需要创建多个独立的任务或模块,每个模块负责特定的功能,并通过某种机制(如消息队列、事件触发等)进行通信。

这种方式可以大大提高程序可扩展性和可移植性。

以下是一个LED和按键代理的简化模型。

cpp 复制代码
#include <reg51.h>  // 包含51系列单片机的寄存器定义

// 假设P3.5是按键输入,P1.0是LED输出
sbit KEY = P3^5;
sbit LED = P1^0;

typedef struct 
{
    unsigned char pin;    // 代理关联的引脚
    void (*action)(void); // 代理的行为函数
} Agent;

// 按键代理的行为函数声明
void keyAction(void);
// LED代理的行为函数声明
void ledAction(void);

// 代理数组,存储所有代理的行为和关联的引脚
Agent agents[] = 
{
    {5, keyAction},  // 按键代理,关联P3.5
    {0, ledAction}   // LED代理,关联P1.0
};

// 按键代理的行为函数
void keyAction(void) 
{
    if(KEY == 0) // 检测按键是否被按下
        {  
        LED = !LED;   // 如果按键被按下,切换LED状态
        while(KEY == 0);  // 等待按键释放
    }
}

// LED代理的行为函数
void ledAction(void) 
{
    static unsigned int toggleCounter = 0;
    toggleCounter++;
    if(toggleCounter == 500)  // 假设每500个时钟周期切换一次LED
        { 
        LED = !LED;               // 切换LED状态
        toggleCounter = 0;        // 重置计数器
    }
}

// 主函数
void main(void) 
{
    unsigned char agentIndex;
    // 主循环
    while(1) 
    {
        for(agentIndex = 0; agentIndex < sizeof(agents) / sizeof(agents[0]); agentIndex++) 
        {
            // 调用每个代理的行为函数
            (*agents[agentIndex].action)(); // 注意函数指针的调用方式
        }
    }
}

9.组件化架构

组件化架构是一种将软件系统分解为独立、可重用组件的方法。

将程序分割成负责特定任务的模块,如LED控制、按键处理、传感器读数等。

每个组件可以独立开发和测试,然后被组合在一起形成完整的系统。

以下是一个简化的组件化架构示例,模拟了一个单片机系统中的LED控制和按键输入处理两个组件。

为了简化,组件间的通信将通过直接函数调用来模拟。

cpp 复制代码
#include <reg51.h>  // 包含51系列单片机的寄存器定义

// 定义组件结构体
typedef struct 
{
    void (*init)(void);      // 组件初始化函数
    void (*task)(void);       // 组件任务函数
} Component;

// 假设P3.5是按键输入,P1.0是LED输出
sbit KEY = P3^5;
sbit LED = P1^0;

// LED组件
void LED_Init(void) 
{
    LED = 0;  // 初始化LED状态为关闭
}

void LED_Task(void) 
{
    static unsigned int toggleCounter = 0;
    toggleCounter++;
    if (toggleCounter >= 1000) // 假设每1000个时钟周期切换一次LED
    {  
        LED = !LED;                // 切换LED状态
        toggleCounter = 0;         // 重置计数器
    }
}

// 按键组件
void KEY_Init(void) 
{
    // 按键初始化代码
}

void KEY_Task(void) 
{
    if (KEY == 0) // 检测按键是否被按下
    {  
       LED = !LED;  // 如果按键被按下,切换LED状态
       while(KEY == 0);  // 等待按键释放
    }
}

// 组件数组,存储系统中所有组件的初始化和任务函数
Component components[] = 
{
    {LED_Init, LED_Task},
    {KEY_Init, KEY_Task}
};

// 系统初始化函数,调用所有组件的初始化函数
void System_Init(void) 
{
    unsigned char componentIndex;
    for (componentIndex = 0; componentIndex < sizeof(components) / sizeof(components[0]); componentIndex++) 
    {
        components[componentIndex].init();
    }
}

// 主循环,调用所有组件的任务函数
void main(void) 
{
    System_Init();  // 系统初始化
    while(1) 
    {
        unsigned char componentIndex;
        for (componentIndex = 0; componentIndex < sizeof(components) / sizeof(components[0]); componentIndex++)
        {
            components[componentIndex].task();  // 调用组件任务
        }
    }
}

以上几种,我都整理到单片机入门到高级资料+工具包了,大家可自行在朋友圈找我安排。

当然,以上都是最简易的代码模型,如果想用于实际项目,很多细节还要优化。

后面为了适应更复杂的项目,我基于以上这几种编程思维,重构了代码,使OS变得移植性和扩展性更强,用起来也更灵活。

我在2019年,也系统录制过关于这套架构的教程,粉丝可找我安排。

目前我们无际单片机特训营项目3和6就是采用这种架构,稳的一批。

如果想系统提升编程思维和代码水平,还是得从0到1去学习我们项目,并不是说技术有多难,而是很多思维和实现细节,没有参考,没人指点,靠自己需要摸索很久。

除了以上架构,更复杂的就是RTOS了。

不过一般对于有架构设计能力的工程师来说,更习惯于使用传统的裸机编程方式,这种方式可能更直观且可控。

相关推荐
国科安芯12 小时前
FreeRTOS 在 AS32系列RISC-V 架构MCU电机驱动中的应用实践与优化
单片机·嵌入式硬件·安全·架构·压力测试·risc-v·安全性测试
随缘体验官12 小时前
【无标题】测试一下
java
染予12 小时前
GPIO中断实现流程
单片机·嵌入式硬件
.柒宇.12 小时前
力扣hoT100之找到字符串中所有字母异位词(java版)
java·数据结构·算法·leetcode
小柯博客13 小时前
STM32MP1 没有硬件编解码,如何用 CPU 实现 H.264 编码支持 WebRTC?
c语言·stm32·嵌入式硬件·webrtc·h.264·h264·v4l2
拂晓银砾13 小时前
Java 连接数据库
java
青衫码上行13 小时前
【Java Web学习 | 第九篇】JavaScript(3) 数组+函数
java·开发语言·前端·javascript·学习
浮游本尊13 小时前
Java学习第29天 - 企业级系统架构与实战
java
程序猿DD13 小时前
探索 Java 中的新 HTTP 客户端
java·后端
m0_4955627814 小时前
Swift-Enum
java·算法·swift