当前位置:   article > 正文

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

单片机程序架构

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

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

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

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

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

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

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

1.线性架构

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

  1. #include <reg51.h> // 包含51系列单片机的寄存器定义
  2. // 延时函数,用于产生一定的延迟
  3. void delay(unsigned int count) {
  4. unsigned int i;
  5. while(count--) {
  6. for(i = 0; i < 120; i++) {} // 空循环,用于产生延迟
  7. }
  8. }
  9. void main() {
  10. // 初始设置P1端口为输出模式,用于控制LED
  11. P1 = 0xFF; // 将P1端口设置为高电平,关闭所有LED
  12. while(1) { // 无限循环
  13. P1 = 0x00; // 将P1端口设置为低电平,点亮所有LED
  14. delay(500000); // 调用延时函数,延迟一段时间
  15. P1 = 0xFF; // 将P1端口设置为高电平,关闭所有LED
  16. delay(500000); // 再次调用延时函数,延迟相同的时间
  17. }
  18. }

2.模块化架构

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

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

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

  1. #include <reg51.h> // 包含51系列单片机的寄存器定义
  2. // 定义信号灯的状态
  3. typedef enum {
  4. RED_LIGHT,
  5. YELLOW_LIGHT,
  6. GREEN_LIGHT
  7. } TrafficLightState;
  8. // 函数声明
  9. void initializeTrafficLight(void);
  10. void setTrafficLight(TrafficLightState state);
  11. void delay(unsigned int milliseconds);
  12. // 信号灯控制主函数
  13. void main(void) {
  14. initializeTrafficLight(); // 初始化交通信号灯
  15. while(1) {
  16. setTrafficLight(RED_LIGHT);
  17. delay(5000); // 红灯亮5秒
  18. setTrafficLight(YELLOW_LIGHT);
  19. delay(2000); // 黄灯亮2秒
  20. setTrafficLight(GREEN_LIGHT);
  21. delay(5000); // 绿灯亮5秒
  22. }
  23. }
  24. // 初始化交通信号灯的函数
  25. void initializeTrafficLight(void) {
  26. // 这里可以添加初始化代码,比如设置端口方向、默认状态等
  27. // 假设P1端口连接了信号灯,初始状态为熄灭(高电平)
  28. P1 = 0xFF;
  29. }
  30. // 设置交通信号灯状态的函数
  31. void setTrafficLight(TrafficLightState state) {
  32. switch(state) {
  33. case RED_LIGHT:
  34. // 设置红灯亮,其他灯灭
  35. P1 = 0b11100000; // 假设低电平有效,这里设置P1.0为低电平,其余为高电平
  36. break;
  37. case YELLOW_LIGHT:
  38. // 设置黄灯亮,其他灯灭
  39. P1 = 0b11011000; // 设置P1.1为低电平,其余为高电平
  40. break;
  41. case GREEN_LIGHT:
  42. // 设置绿灯亮,其他灯灭
  43. P1 = 0b11000111; // 设置P1.2为低电平,其余为高电平
  44. break;
  45. default:
  46. // 默认为熄灭所有灯
  47. P1 = 0xFF;
  48. break;
  49. }
  50. }
  51. // 延时函数,参数是毫秒数
  52. void delay(unsigned int milliseconds) {
  53. unsigned int delayCount = 0;
  54. while(milliseconds--) {
  55. for(delayCount = 0; delayCount < 120; delayCount++) {
  56. // 空循环,用于产生延时
  57. }
  58. }
  59. }

3.层次化架构

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

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

  1. #include <reg51.h> // 包含51系列单片机的寄存器定义
  2. // 定义不同的操作级别
  3. typedef enum {
  4. LEVEL_USER,
  5. LEVEL_ADMIN,
  6. LEVEL_SUPERUSER
  7. } OperationLevel;
  8. // 函数声明
  9. void systemInit(void);
  10. void performOperation(OperationLevel level);
  11. void displayMessage(char* message);
  12. // 系统初始化后的主循环
  13. void main(void) {
  14. systemInit(); // 系统初始化
  15. // 模拟用户操作
  16. performOperation(LEVEL_USER);
  17. // 模拟管理员操作
  18. performOperation(LEVEL_ADMIN);
  19. // 模拟超级用户操作
  20. performOperation(LEVEL_SUPERUSER);
  21. while(1) {
  22. // 主循环可以是空闲循环或者处理其他低优先级任务
  23. }
  24. }
  25. // 系统初始化函数
  26. void systemInit(void) {
  27. // 初始化系统资源,如设置端口、中断等
  28. // 这里省略具体的初始化代码
  29. }
  30. // 执行不同级别操作的函数
  31. void performOperation(OperationLevel level) {
  32. switch(level) {
  33. case LEVEL_USER:
  34. //用户操作具体代码
  35. break;
  36. case LEVEL_ADMIN:
  37. //管理员操作具体代码
  38. break;
  39. case LEVEL_SUPERUSER:
  40. //超级用户操作具体代码
  41. break;
  42. }
  43. }
  44. // 显示消息的函数
  45. void displayMessage(char* message) {
  46. // 这里省略了实际的显示代码,因为单片机通常没有直接的屏幕输出
  47. // 消息可以通过LED闪烁、串口输出或其他方式展示
  48. // 假设通过P1端口的LED展示,每个字符对应一个LED闪烁模式
  49. // 实际应用中,需要根据硬件设计来实现消息的显示
  50. }

4.事件驱动架构

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

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

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

  1. #include <reg51.h> // 包含51系列单片机的寄存器定义
  2. // 定义按键和LED的状态
  3. #define KEY_PORT P3 // 假设按键连接在P3端口
  4. #define LED_PORT P2 // 假设LED连接在P2端口
  5. // 函数声明
  6. void delay(unsigned int milliseconds);
  7. bit checkKeyPress(void); // 返回按键是否被按下的状态(1表示按下,0表示未按下)
  8. // 定时器初始化函数
  9. void timer0Init(void)
  10. {
  11. TMOD = 0x01; // 设置定时器模式寄存器,使用模式1(16位定时器)
  12. TH0 = 0xFC; // 设置定时器初值,用于产生定时中断
  13. TL0 = 0x18;
  14. ET0 = 1; // 开启定时器0中断
  15. EA = 1; // 开启总中断
  16. TR0 = 1; // 启动定时器
  17. }
  18. // 定时器中断服务程序
  19. void timer0_ISR() interrupt 1
  20. {
  21. // 定时器溢出后自动重新加载初值,无需手动重置
  22. // 这里可以放置定时器溢出后需要执行的代码
  23. }
  24. // 按键中断服务程序
  25. bit keyPress_ISR(void) interrupt 2 using 1
  26. {
  27. if(KEY_PORT != 0xFF) // 检测是否有按键按下
  28. {
  29. LED_PORT = ~LED_PORT; // 如果有按键按下,切换LED状态
  30. delay(20); // 去抖动延时
  31. while(KEY_PORT != 0xFF); // 等待按键释放
  32. return 1; // 返回按键已按下
  33. }
  34. return 0; // 如果没有按键按下,返回0
  35. }
  36. // 延时函数,参数是毫秒数
  37. void delay(unsigned int milliseconds) {
  38. unsigned int i, j;
  39. for(i = 0; i < milliseconds; i++)
  40. for(j = 0; j < 1200; j++); // 空循环,用于产生延时
  41. }
  42. // 主函数
  43. void main(void)
  44. {
  45. timer0Init(); // 初始化定时器
  46. LED_PORT = 0xFF; // 初始LED熄灭(假设低电平点亮LED)
  47. while(1)
  48. {
  49. if(checkKeyPress())
  50. { // 检查是否有按键按下事件
  51. // 如果有按键按下,这里可以添加额外的处理代码
  52. }
  53. }
  54. }
  55. // 检查按键是否被按下的函数
  56. bit checkKeyPress(void)
  57. {
  58. bit keyState = 0;
  59. // 模拟按键中断触发,实际应用中需要连接硬件中断
  60. if(1) // 假设按键中断触发
  61. {
  62. keyState = keyPress_ISR(); // 调用按键中断服务程序
  63. }
  64. return keyState; // 返回按键状态
  65. }

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

5.状态机架构

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

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

  1. #include <reg51.h> // 包含51系列单片机的寄存器定义
  2. // 定义自动售货机的状态
  3. typedef enum {
  4. IDLE,
  5. COIN_INSERTED,
  6. PRODUCT_SELECTED,
  7. DISPENSE,
  8. CHANGE_RETURNED
  9. } VendingMachineState;
  10. // 定义事件
  11. typedef enum {
  12. COIN_EVENT,
  13. PRODUCT_EVENT,
  14. DISPENSE_EVENT,
  15. REFUND_EVENT
  16. } VendingMachineEvent;
  17. // 函数声明
  18. void processEvent(VendingMachineEvent event);
  19. void dispenseProduct(void);
  20. void returnChange(void);
  21. // 当前状态
  22. VendingMachineState currentState = IDLE;
  23. // 主函数
  24. void main(void)
  25. {
  26. // 初始化代码(如果有)
  27. // ...
  28. while(1)
  29. {
  30. // 假设事件由外部触发,这里使用一个模拟事件
  31. VendingMachineEvent currentEvent = COIN_EVENT; // 模拟投入硬币事件
  32. processEvent(currentEvent); // 处理当前事件
  33. }
  34. }
  35. // 处理事件的函数
  36. void processEvent(VendingMachineEvent event)
  37. {
  38. switch(currentState)
  39. {
  40. case IDLE:
  41. if(event == COIN_EVENT)
  42. {
  43. // 如果在空闲状态且检测到硬币投入事件,则转换到硬币投入状态
  44. currentState = COIN_INSERTED;
  45. }
  46. break;
  47. case COIN_INSERTED:
  48. if(event == PRODUCT_EVENT)
  49. {
  50. // 如果在硬币投入状态且用户选择商品,则请求出货
  51. currentState = PRODUCT_SELECTED;
  52. }
  53. break;
  54. case PRODUCT_SELECTED:
  55. if(event == DISPENSE_EVENT)
  56. {
  57. dispenseProduct(); // 出货商品
  58. currentState = DISPENSE;
  59. }
  60. break;
  61. case DISPENSE:
  62. if(event == REFUND_EVENT)
  63. {
  64. returnChange(); // 返回找零
  65. currentState = CHANGE_RETURNED;
  66. }
  67. break;
  68. case CHANGE_RETURNED:
  69. // 等待下一个循环,返回到IDLE状态
  70. currentState = IDLE;
  71. break;
  72. default:
  73. // 如果状态非法,重置为IDLE状态
  74. currentState = IDLE;
  75. break;
  76. }
  77. }
  78. // 出货商品的函数
  79. void dispenseProduct(void)
  80. {
  81. // 这里添加出货逻辑,例如激活电机推出商品
  82. // 假设P1端口连接了出货电机
  83. P1 = 0x00; // 激活电机
  84. // ... 出货逻辑
  85. P1 = 0xFF; // 关闭电机
  86. }
  87. // 返回找零的函数
  88. void returnChange(void)
  89. {
  90. // 这里添加找零逻辑,例如激活机械臂放置零钱
  91. // 假设P2端口连接了找零机械臂
  92. P2 = 0x00; // 激活机械臂
  93. // ... 找零逻辑
  94. P2 = 0xFF; // 关闭机械臂
  95. }

6.面向对象架构

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

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

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

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

  1. #include <reg51.h>
  2. // 定义一个LED类
  3. typedef struct {
  4. unsigned char state; // LED的状态
  5. unsigned char pin; // LED连接的引脚
  6. void (*turnOn)(struct LED*); // 点亮LED的方法
  7. void (*turnOff)(struct LED*); // 熄灭LED的方法
  8. } LED;
  9. // LED类的构造函数
  10. void LED_Init(LED* led, unsigned char pin) {
  11. led->state = 0; // 默认状态为熄灭
  12. led->pin = pin; // 设置LED连接的引脚
  13. }
  14. // 点亮LED的方法
  15. void LED_TurnOn(LED* led) {
  16. // 根据引脚状态点亮LED
  17. if(led->pin < 8) {
  18. P0 |= (1 << led->pin); // 假设P0.0到P0.7连接了8个LED
  19. } else {
  20. P1 &= ~(1 << (led->pin - 8)); // 假设P1.0到P1.7连接了另外8个LED
  21. }
  22. led->state = 1; // 更新状态为点亮
  23. }
  24. // 熄灭LED的方法
  25. void LED_TurnOff(LED* led) {
  26. // 根据引脚状态熄灭LED
  27. if(led->pin < 8) {
  28. P0 &= ~(1 << led->pin); // 熄灭P0上的LED
  29. } else {
  30. P1 |= (1 << (led->pin - 8)); // 熄灭P1上的LED
  31. }
  32. led->state = 0; // 更新状态为熄灭
  33. }
  34. // 主函数
  35. void main(void) {
  36. LED myLed; // 创建一个LED对象
  37. LED_Init(&myLed, 3); // 初始化LED对象,连接在P0.3
  38. // 给LED对象绑定方法
  39. myLed.turnOn = LED_TurnOn;
  40. myLed.turnOff = LED_TurnOff;
  41. // 使用面向对象的风格控制LED
  42. while(1) {
  43. myLed.turnOn(&myLed); // 点亮LED
  44. // 延时
  45. myLed.turnOff(&myLed); // 熄灭LED
  46. // 延时
  47. }
  48. }

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

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

7.基于任务的架构

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

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

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

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

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

  1. #include <reg51.h>
  2. // 假设P1.0是LED输出
  3. sbit LED = P1^0;
  4. // 全局变量,用于记录系统Tick
  5. unsigned int systemTick = 0;
  6. // 任务函数声明
  7. void taskLEDBlink(void);
  8. void taskKeyScan(void);
  9. // 定时器0中断服务程序,用于产生Tick
  10. void timer0_ISR() interrupt 1 using 1
  11. {
  12. // 定时器溢出后自动重新加载初值,无需手动重置
  13. systemTick++; // 更新系统Tick计数器
  14. }
  15. // 任务调度器,主函数中调用,负责任务轮询
  16. void taskScheduler(void)
  17. {
  18. // 检查系统Tick,决定是否执行任务
  19. // 例如,如果我们需要每1000个Tick执行一次LED闪烁任务
  20. if (systemTick % 1000 == 0)
  21. {
  22. taskLEDBlink();
  23. }
  24. // 如果有按键任务,可以类似地检查Tick并执行
  25. if (systemTick % 10 == 0)
  26. {
  27. taskKeyScan();
  28. }
  29. }
  30. // LED闪烁任务
  31. void taskLEDBlink(void)
  32. {
  33. static bit ledState = 0; // 用于记录LED的当前状态
  34. ledState = !ledState; // 切换LED状态
  35. LED = ledState; // 更新LED硬件状态
  36. }
  37. // 按键扫描任务(示例中省略具体实现)
  38. void taskKeyScan(void)
  39. {
  40. // 按键扫描逻辑
  41. }
  42. // 主函数
  43. void main(void)
  44. {
  45. // 初始化LED状态
  46. LED = 0;
  47. // 定时器0初始化设置
  48. TMOD &= 0xF0; // 设置定时器模式寄存器,使用模式1(16位定时器/计数器)
  49. TH0 = 0x4C; // 设置定时器初值,产生定时中断(定时周期取决于系统时钟频率)
  50. TL0 = 0x00;
  51. ET0 = 1; // 允许定时器0中断
  52. EA = 1; // 允许中断
  53. TR0 = 1; // 启动定时器0
  54. while(1)
  55. {
  56. taskScheduler(); // 调用任务调度器
  57. }
  58. }

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

8.代理架构

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

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

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

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

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

  1. #include <reg51.h> // 包含51系列单片机的寄存器定义
  2. // 假设P3.5是按键输入,P1.0是LED输出
  3. sbit KEY = P3^5;
  4. sbit LED = P1^0;
  5. typedef struct
  6. {
  7. unsigned char pin; // 代理关联的引脚
  8. void (*action)(void); // 代理的行为函数
  9. } Agent;
  10. // 按键代理的行为函数声明
  11. void keyAction(void);
  12. // LED代理的行为函数声明
  13. void ledAction(void);
  14. // 代理数组,存储所有代理的行为和关联的引脚
  15. Agent agents[] =
  16. {
  17. {5, keyAction}, // 按键代理,关联P3.5
  18. {0, ledAction} // LED代理,关联P1.0
  19. };
  20. // 按键代理的行为函数
  21. void keyAction(void)
  22. {
  23. if(KEY == 0) // 检测按键是否被按下
  24. {
  25. LED = !LED; // 如果按键被按下,切换LED状态
  26. while(KEY == 0); // 等待按键释放
  27. }
  28. }
  29. // LED代理的行为函数
  30. void ledAction(void)
  31. {
  32. static unsigned int toggleCounter = 0;
  33. toggleCounter++;
  34. if(toggleCounter == 500) // 假设每500个时钟周期切换一次LED
  35. {
  36. LED = !LED; // 切换LED状态
  37. toggleCounter = 0; // 重置计数器
  38. }
  39. }
  40. // 主函数
  41. void main(void)
  42. {
  43. unsigned char agentIndex;
  44. // 主循环
  45. while(1)
  46. {
  47. for(agentIndex = 0; agentIndex < sizeof(agents) / sizeof(agents[0]); agentIndex++)
  48. {
  49. // 调用每个代理的行为函数
  50. (*agents[agentIndex].action)(); // 注意函数指针的调用方式
  51. }
  52. }
  53. }

9.组件化架构

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

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

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

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

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

  1. #include <reg51.h> // 包含51系列单片机的寄存器定义
  2. // 定义组件结构体
  3. typedef struct
  4. {
  5. void (*init)(void); // 组件初始化函数
  6. void (*task)(void); // 组件任务函数
  7. } Component;
  8. // 假设P3.5是按键输入,P1.0是LED输出
  9. sbit KEY = P3^5;
  10. sbit LED = P1^0;
  11. // LED组件
  12. void LED_Init(void)
  13. {
  14. LED = 0; // 初始化LED状态为关闭
  15. }
  16. void LED_Task(void)
  17. {
  18. static unsigned int toggleCounter = 0;
  19. toggleCounter++;
  20. if (toggleCounter >= 1000) // 假设每1000个时钟周期切换一次LED
  21. {
  22. LED = !LED; // 切换LED状态
  23. toggleCounter = 0; // 重置计数器
  24. }
  25. }
  26. // 按键组件
  27. void KEY_Init(void)
  28. {
  29. // 按键初始化代码
  30. }
  31. void KEY_Task(void)
  32. {
  33. if (KEY == 0) // 检测按键是否被按下
  34. {
  35. LED = !LED; // 如果按键被按下,切换LED状态
  36. while(KEY == 0); // 等待按键释放
  37. }
  38. }
  39. // 组件数组,存储系统中所有组件的初始化和任务函数
  40. Component components[] =
  41. {
  42. {LED_Init, LED_Task},
  43. {KEY_Init, KEY_Task}
  44. };
  45. // 系统初始化函数,调用所有组件的初始化函数
  46. void System_Init(void)
  47. {
  48. unsigned char componentIndex;
  49. for (componentIndex = 0; componentIndex < sizeof(components) / sizeof(components[0]); componentIndex++)
  50. {
  51. components[componentIndex].init();
  52. }
  53. }
  54. // 主循环,调用所有组件的任务函数
  55. void main(void)
  56. {
  57. System_Init(); // 系统初始化
  58. while(1)
  59. {
  60. unsigned char componentIndex;
  61. for (componentIndex = 0; componentIndex < sizeof(components) / sizeof(components[0]); componentIndex++)
  62. {
  63. components[componentIndex].task(); // 调用组件任务
  64. }
  65. }
  66. }

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

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

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

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

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

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

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

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

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/人工智能uu/article/detail/795595
推荐阅读
相关标签
  

闽ICP备14008679号