长文预警,加代码5000多字,写了4个多小时,盘软件架构,这篇文章就够了!
可能很多工程师,工作了很多年,都不会有软件架构的概念。
因为我在做研发工程师的第6年,才开始意识到这个东西,在此之前,都是做一些比较简单的项目,一个main函数干到底,架构复杂了反而是累赘。
后面有幸,接触了稍微复杂点的项目,感觉以前水平hold不住,然后借着项目需求,学习了很多优秀的代码架构,比如以前同事的,一些模组厂的sdk,还有市面上成熟的系统。
说出来可能有点夸张,一个好项目带来的成长,顶你做几年小项目。
在一个工程师从入门到成为高级工程师,都会经历哪些软件架构?
下面给大家盘点一下,每个都提供了简易的架构模型代码。
1.线性架构
这是最简单的一种程序设计方法,也就是我们在入门时写的,下面是一个使用c语言编写的线性架构示例:
#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语言编写的模块化架构示例,该程序模拟了一个简单的交通信号灯控制系统。
#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语言编写的层次化架构示例,模拟了一个具有不同权限级别的嵌入式系统。
#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控制。
#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)的示例,模拟了一个简单的自动售货机的状态转换。
#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类。
#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闪烁任务。
#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和按键代理的简化模型。
#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控制和按键输入处理两个组件。
为了简化,组件间的通信将通过直接函数调用来模拟。
#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了。
不过一般对于有架构设计能力的工程师来说,更习惯于使用传统的裸机编程方式,这种方式可能更直观且可控。
发表评论