编程技术分享

分享编程知识,探讨技术创新

0%

简介:专为FDM-3D打印机设计的全手搓耗材干燥箱,PID恒温控制,0.91寸OLED温湿度显示,热风循环烘干耗材。外壳结构均可使用合适的材料3D打印出来。温控板程序简单修改还可用于鸡鸭鹅蛋等孵化控制器。

我将针对这款FDM-3D打印机耗材干燥箱,从需求分析到系统实现,再到测试验证和维护升级,详细阐述如何构建一个可靠、高效、可扩展的系统平台。
关注微信公众号,提前获取相关推文

一、 需求分析

  1. 核心功能:

    • 恒温控制: 使用PID算法精确控制干燥箱内部温度,确保耗材在最佳环境下干燥。
    • 热风循环: 通过风扇产生热风循环,均匀加热箱内耗材,避免局部温度过高或过低。
    • 温湿度显示: 使用0.91寸OLED屏幕实时显示箱内温度和湿度。
    • 用户交互: 简单的按键操作,用于设置目标温度,以及查看当前状态。
  2. 扩展需求:

    • 多档温度设置: 提供多档预设温度,适应不同耗材的干燥需求。
    • 定时功能: 可以设置定时烘干时间,避免长时间加热。
    • 异常报警: 当温度过高或过低时发出报警。
    • 自适应PID: PID参数可以根据实际环境进行自适应调整。
    • 远程监控: 可选,未来可增加WiFi模块实现远程监控。
  3. 可靠性要求:

    • 安全可靠: 系统运行稳定,避免过热、短路等安全隐患。
    • 精度要求: 温度控制精度在±1℃内。
    • 长时间运行: 能够长时间稳定工作。
  4. 可扩展性要求:

    • 软件架构: 软件架构模块化、易于扩展和维护。
    • 硬件接口: 预留足够的硬件接口,方便添加新功能。

二、 系统架构设计

  1. 硬件架构:

    • 主控芯片: 选择低功耗的单片机,如STM32F103C8T6(或ESP32等),具有足够的处理能力和外设接口。
    • 温度传感器: 使用高精度温湿度传感器,如DHT22或SHT30。
    • 加热模块: 使用PTC加热片或加热丝,配合PWM调速控制加热功率。
    • 风扇: 使用直流风扇,配合PWM调速控制风速。
    • OLED显示屏: 使用0.91寸I2C OLED显示屏。
    • 按键: 若干按键用于用户交互。
    • 电源模块: 为整个系统提供稳定的电源。
  2. 软件架构:

    • 分层架构: 采用分层架构,将系统划分为硬件抽象层(HAL)、驱动层、服务层和应用层。
    • 模块化设计: 每个功能模块都是独立的,方便开发和维护。
    • 状态机: 使用状态机管理系统运行状态。
    • 实时操作系统(RTOS): 可以考虑使用 FreeRTOS 或类似 RTOS 来管理任务。

三、 详细的代码设计架构及C代码实现

1. 硬件抽象层 (HAL)

  • 目的: 屏蔽底层硬件差异,提供统一的接口给上层使用。
  • 文件: hal.h, hal.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// hal.h
#ifndef __HAL_H__
#define __HAL_H__

#include <stdint.h>
#include <stdbool.h>

// GPIO配置
#define HEATER_PIN // 定义加热器控制引脚
#define FAN_PIN // 定义风扇控制引脚
#define BUTTON1_PIN // 定义按键1引脚
#define BUTTON2_PIN // 定义按键2引脚
#define BUTTON3_PIN // 定义按键3引脚

// 定义高低电平
#define HIGH 1
#define LOW 0

// 定义引脚模式
typedef enum {
GPIO_MODE_INPUT,
GPIO_MODE_OUTPUT
} gpio_mode_t;

// 初始化GPIO
void hal_gpio_init(uint16_t pin, gpio_mode_t mode);

// 设置GPIO输出电平
void hal_gpio_set_output(uint16_t pin, uint8_t level);

// 读取GPIO输入电平
uint8_t hal_gpio_get_input(uint16_t pin);

// 延时函数
void hal_delay_ms(uint32_t ms);

// PWM 配置
void hal_pwm_init(uint16_t pin, uint32_t frequency); // 初始化PWM
void hal_pwm_set_duty(uint16_t pin, uint16_t duty); // 设置PWM占空比

// I2C 配置
void hal_i2c_init(); // 初始化I2C
void hal_i2c_write(uint8_t addr, uint8_t reg, uint8_t data); // I2C写入
void hal_i2c_read(uint8_t addr, uint8_t reg, uint8_t *data, uint8_t len); // I2C读取

// ADC 配置
void hal_adc_init(); // 初始化ADC
uint16_t hal_adc_read(uint16_t channel); // ADC读取

//串口配置
void hal_uart_init(uint32_t baudrate); //初始化串口
void hal_uart_send_byte(uint8_t data); //发送一个字节
void hal_uart_send_string(const char *str); //发送字符串
uint8_t hal_uart_receive_byte();//接收一个字节
#endif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// hal.c
#include "hal.h"
// 包含你使用的单片机特定的头文件,例如
// #include "stm32f10x.h" // STM32
// #include "esp32/esp32.h" // ESP32

// GPIO 初始化
void hal_gpio_init(uint16_t pin, gpio_mode_t mode) {
// 配置特定单片机的GPIO模式
// 例如:
// if (pin == HEATER_PIN) {
// } else if (pin == FAN_PIN) {
// }
//else if...
// 根据mode配置输入输出模式
}

// 设置GPIO输出
void hal_gpio_set_output(uint16_t pin, uint8_t level) {
// 配置特定单片机的GPIO输出
//例如:
// if(pin== HEATER_PIN)
// {
// if(level == HIGH)
// GPIOA->BSRR = GPIO_BSRR_BS12; //例如点亮LED
// else
// GPIOA->BSRR = GPIO_BSRR_BR12; // 例如关闭LED
// }
}

// 读取GPIO输入
uint8_t hal_gpio_get_input(uint16_t pin) {
// 读取特定单片机的GPIO输入
//例如:
// if(pin== BUTTON1_PIN)
// return (GPIOA->IDR & GPIO_IDR_IDR1);
//else if ...
return 0;
}

// 延时函数
void hal_delay_ms(uint32_t ms) {
// 实现延时函数,例如使用单片机的定时器或循环
for(int i=0;i<ms*1000;i++);
}

// PWM 初始化
void hal_pwm_init(uint16_t pin, uint32_t frequency) {
// 初始化特定单片机的PWM
}

// PWM 设置占空比
void hal_pwm_set_duty(uint16_t pin, uint16_t duty) {
// 设置特定单片机的PWM占空比
}

//I2C 初始化
void hal_i2c_init(){
// 初始化特定单片机的I2C
}
// I2C写入
void hal_i2c_write(uint8_t addr, uint8_t reg, uint8_t data){
// I2C写入
}
// I2C读取
void hal_i2c_read(uint8_t addr, uint8_t reg, uint8_t *data, uint8_t len){
// I2C读取
}

// ADC 初始化
void hal_adc_init(){
// 初始化特定单片机的ADC
}

// ADC 读取
uint16_t hal_adc_read(uint16_t channel){
// ADC 读取特定通道的值
return 0;
}

//串口初始化
void hal_uart_init(uint32_t baudrate){
// 初始化串口
}
//串口发送一个字节
void hal_uart_send_byte(uint8_t data){
// 发送一个字节
}

//串口发送字符串
void hal_uart_send_string(const char *str){
while(*str!='\0'){
hal_uart_send_byte(*str++);
}
}

//串口接收一个字节
uint8_t hal_uart_receive_byte(){
return 0;//接收一个字节
}

2. 驱动层

  • 目的: 封装硬件的具体操作,为上层提供统一的驱动接口。
  • 文件: driver_sensor.h, driver_sensor.c, driver_display.h, driver_display.c, driver_heater.h, driver_heater.c, driver_fan.h,driver_fan.c,driver_button.h,driver_button.c

2.1 传感器驱动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// driver_sensor.h
#ifndef __DRIVER_SENSOR_H__
#define __DRIVER_SENSOR_H__

#include <stdint.h>

typedef struct {
float temperature;
float humidity;
} sensor_data_t;

// 初始化传感器
void sensor_init();

// 读取传感器数据
bool sensor_read(sensor_data_t *data);

#endif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// driver_sensor.c
#include "driver_sensor.h"
#include "hal.h"
#include <stdio.h>

// DHT22或其他传感器相关的宏定义和变量

#define DHT22_ADDR 0x20 // 传感器I2C地址
uint8_t data[5] = {0,0,0,0,0};

// 初始化传感器
void sensor_init() {
// 例如,如果是DHT22,则初始化GPIO
hal_i2c_init(); // 初始化I2C
}


// 读取传感器数据
bool sensor_read(sensor_data_t *data) {
hal_i2c_read(DHT22_ADDR,0,data,5);
if(data[0]==0&&data[1]==0&&data[2]==0&&data[3]==0)
return false;
data->temperature = (float)((data[2] << 8) | data[3]) / 10.0f;
data->humidity = (float)((data[0] << 8) | data[1]) / 10.0f;
return true;
}

2.2 OLED显示驱动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// driver_display.h
#ifndef __DRIVER_DISPLAY_H__
#define __DRIVER_DISPLAY_H__

#include <stdint.h>
#include <stdbool.h>

// 初始化显示
void display_init();

// 清除显示
void display_clear();

// 显示字符串
void display_string(int x, int y, const char *str);

// 显示数字
void display_number(int x, int y, int32_t num);

//显示温度浮点型
void display_float(int x, int y, float num);

// 更新显示
void display_update();

#endif

// driver_display.c
#include "driver_display.h"
#include "hal.h"
#include <stdio.h>
#include <string.h>

#define OLED_ADDR 0x3C // OLED的I2C地址
#define OLED_WIDTH 128 // OLED宽度
#define OLED_HEIGHT 64 // OLED高度

//OLED初始化数据
static const uint8_t OLED_INIT_CMD[] = {
    0xAE,       //--display off
    0x00,       //---set low column address
    0x10,       //---set high column address
    0x40,       //--set start line address  Set Mapping RAM Display Start Line (0x00~0x3F)
    0xB0,       //--set page address
    0x81,       //--set contrast control register
    0xCF,       // Set SEG Output Current Brightness
    0xA1,       //--Set SEG/Column Mapping     0xa0左右反置 0xa1正常
    0xC8,       //Set COM/Row Scan Direction   0xc0上下反置 0xc8正常
    0xA6,       //--set normal display
    0xA8,       //--set multiplex ratio(1 to 64)
    0x3f,       //--1/64 duty
    0xD3,       //-set display offset  Shift Mapping RAM Counter (0x00~0x3F)
    0x00,       //-not offset
    0xd5,       //--set display clock divide ratio/oscillator frequency
    0x80,       //--set divide ratio, Set Clock as 100 Frames/Sec
    0xD9,       //--set pre-charge period
    0xF1,       //Set Pre-Charge as 15 Clocks & Discharge as 1 Clock
    0xDA,       //--set com pins hardware configuration
    0x12,
    0xDB,       //--set vcomh
    0x40,       //Set VCOM Deselect Level
    0x20,       //-Set Page Addressing Mode (0x00/0x01/0x02)
    0x02,       //
    0x8D,       //--set Charge Pump enable/disable
    0x14,       //--set(0x10) disable
    0xA4,       // Disable Entire Display On (0xa4=Normal, 0xa5=All On)
    0xA6,
    0xAF        //--display on
};
//OLED显存
uint8_t oled_buffer[OLED_WIDTH * OLED_HEIGHT / 8];

void display_write_cmd(uint8_t cmd) {
  hal_i2c_write(OLED_ADDR, 0x00, cmd);
}

void display_write_data(uint8_t data) {
  hal_i2c_write(OLED_ADDR, 0x40, data);
}


// 初始化显示
void display_init() {
    hal_i2c_init();
    hal_delay_ms(100);
    for (int i = 0; i < sizeof(OLED_INIT_CMD); i++) {
       display_write_cmd(OLED_INIT_CMD[i]);
   }
    display_clear();
    display_update();
}

// 清除显示
void display_clear() {
    memset(oled_buffer, 0x00, sizeof(oled_buffer));
}

// 显示字符串
void display_string(int x, int y, const char *str) {
   uint8_t c;
   while (*str) {
        c = *str++;
        if (c < 32 || c > 127) continue;
        for (int i = 0; i < 8; i++) {
            uint8_t line = (uint8_t)font8x16[(c - 32) * 16 + i*2];
            uint8_t line2 = (uint8_t)font8x16[(c - 32) * 16 + i*2+1];
            if (y + 2*i < OLED_HEIGHT)
            {
              oled_buffer[(y + 2 * i) / 8 * OLED_WIDTH + x + 0] |= line;
              oled_buffer[(y + 2 * i + 1) / 8 * OLED_WIDTH + x + 0] |= line2;
            }
            
        }
        x += 8;
        if (x >= OLED_WIDTH) break; // 超出屏幕宽度,停止显示
    }

}

// 显示数字
void display_number(int x, int y, int32_t num) {
    char buffer[12];
    sprintf(buffer, "%ld", num);
    display_string(x, y, buffer);
}

//显示温度浮点型
void display_float(int x, int y, float num){
    char buffer[12];
    sprintf(buffer, "%.1f", num);
    display_string(x, y, buffer);
}
// 更新显示
void display_update() {
    for (int i = 0; i < OLED_HEIGHT / 8; i++) {
       display_write_cmd(0xb0 + i); // 设置页地址
       display_write_cmd(0x00);     // 设置列地址低四位
       display_write_cmd(0x10);     // 设置列地址高四位

       for (int j = 0; j < OLED_WIDTH; j++) {
            display_write_data(oled_buffer[i * OLED_WIDTH + j]);
        }
    }
}

const uint8_t font8x16[][16] = {
    // 0x20 Space
   { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},

    // 0x21 !
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x38, 0x38, 0x00, 0x00, 0x00},

    // 0x22 "
    { 0x00, 0x00, 0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x30, 0x30, 0x00, 0x00},

    // 0x23 #
    { 0x00, 0x38, 0x38, 0x7e, 0x38, 0x38, 0x38, 0x00, 0x00, 0x38, 0x38, 0x7e, 0x38, 0x38, 0x38, 0x00},

    // 0x24 $
    { 0x00, 0x00, 0x30, 0x7c, 0x06, 0x7c, 0x18, 0x00, 0x00, 0x18, 0x7c, 0x06, 0x7c, 0x30, 0x00, 0x00},

    // 0x25 %
    { 0x00, 0x63, 0x30, 0x18, 0x0c, 0x63, 0x00, 0x00, 0x00, 0x4e, 0x0c, 0x18, 0x30, 0x63, 0x00, 0x00},

    // 0x26 &
    { 0x00, 0x38, 0x54, 0x54, 0x1c, 0x40, 0x3c, 0x00, 0x00, 0x0c, 0x2a, 0x2a, 0x1e, 0x01, 0x3e, 0x00},

    // 0x27 '
    { 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00},

    // 0x28 (
    { 0x00, 0x00, 0x0c, 0x18, 0x30, 0x60, 0x00, 0x00, 0x00, 0x00, 0x60, 0x30, 0x18, 0x0c, 0x00, 0x00},

    // 0x29 )
    { 0x00, 0x00, 0x60, 0x30, 0x18, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x18, 0x30, 0x60, 0x00, 0x00},

    // 0x2a *
    { 0x00, 0x18, 0x3c, 0x7e, 0x3c, 0x18, 0x00, 0x00, 0x00, 0x18, 0x3c, 0x7e, 0x3c, 0x18, 0x00, 0x00},

    // 0x2b +
    { 0x00, 0x10, 0x10, 0x3c, 0x10, 0x10, 0x00, 0x00, 0x00, 0x10, 0x10, 0x3c, 0x10, 0x10, 0x00, 0x00},

    // 0x2c ,
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x18, 0x30, 0x00, 0x00, 0x00},

    // 0x2d -
    { 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00},

    // 0x2e .
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00},

    // 0x2f /
    { 0x00, 0x00, 0x00, 0x40, 0x20, 0x10, 0x00, 0x00, 0x00, 0x00, 0x04, 0x08, 0x10, 0x00, 0x00, 0x00},

    // 0x30 0
    { 0x00, 0x3c, 0x66, 0x66, 0x66, 0x3c, 0x00, 0x00, 0x00, 0x3e, 0x63, 0x63, 0x63, 0x3e, 0x00, 0x00},

    // 0x31 1
    { 0x00, 0x0c, 0x0c, 0x7c, 0x0c, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x18, 0x7f, 0x00, 0x00, 0x00},
    // 0x32 2
    { 0x00, 0x3c, 0x42, 0x32, 0x2a, 0x3c, 0x00, 0x00, 0x00, 0x7e, 0x03, 0x1c, 0x60, 0x7f, 0x00, 0x00},
    // 0x33 3
    { 0x00, 0x3c, 0x42, 0x4a, 0x4a, 0x3c, 0x00, 0x00, 0x00, 0x7e, 0x03, 0x3c, 0x03, 0x7e, 0x00, 0x00},
    // 0x34 4
    { 0x00, 0x06, 0x0e, 0x16, 0x7e, 0x06, 0x00, 0x00, 0x00, 0x18, 0x19, 0x1b, 0x7f, 0x00, 0x00, 0x00},
    // 0x35 5
    { 0x00, 0x7e, 0x4a, 0x4a, 0x4a, 0x3e, 0x00, 0x00, 0x00, 0x7f, 0x40, 0x7c, 0x03, 0x7e, 0x00, 0x00},
    // 0x36 6
    { 0x00, 0x3c, 0x4a, 0x4a, 0x4a, 0x3c, 0x00, 0x00, 0x00, 0x7e, 0x40, 0x7c, 0x43, 0x7e, 0x00, 0x00},
    // 0x37 7
    { 0x00, 0x7e, 0x02, 0x04, 0x08, 0x10, 0x00, 0x00, 0x00, 0x7f, 0x01, 0x02, 0x04, 0x08, 0x00, 0x00},
    // 0x38 8
    { 0x00, 0x3c, 0x4a, 0x4a, 0x4a, 0x3c, 0x00, 0x00, 0x00, 0x7e, 0x43, 0x7e, 0x43, 0x7e, 0x00, 0x00},
    // 0x39 9
    { 0x00, 0x3c, 0x4a, 0x4a, 0x4a, 0x3c, 0x00, 0x00, 0x00, 0x7f, 0x03, 0x7e, 0x40, 0x7e, 0x00, 0x00},

    // 0x3a :
    { 0x00, 0x00, 0x18, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x18, 0x00, 0x00, 0x00},
    
    // 0x3b ;
    { 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00},

    // 0x3c <
    { 0x00, 0x00, 0x10, 0x28, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x0a, 0x11, 0x00, 0x00, 0x00},

    // 0x3d =
    { 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x28, 0x28, 0x28, 0x28, 0x28, 0x00, 0x00},

    // 0x3e >
    { 0x00, 0x00, 0x44, 0x28, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x0a, 0x04, 0x00, 0x00, 0x00},

    // 0x3f ?
    { 0x00, 0x3c, 0x42, 0x02, 0x18, 0x00, 0x00, 0x00, 0x00, 0x7e, 0x03, 0x0c, 0x00, 0x00, 0x00, 0x00},

    // 0x40 @
    { 0x00, 0x3c, 0x42, 0x54, 0x54, 0x7c, 0x00, 0x00, 0x00, 0x7e, 0x01, 0x31, 0x41, 0x7f, 0x00, 0x00},

    // 0x41 A
    { 0x00, 0x3c, 0x44, 0x44, 0x44, 0x7e, 0x00, 0x00, 0x00, 0x7f, 0x04, 0x08, 0x10, 0x7f, 0x00, 0x00},
    // 0x42 B
    { 0x00, 0x7c, 0x4a, 0x4a, 0x4a, 0x3c, 0x00, 0x00, 0x00, 0x7f, 0x49, 0x49, 0x49, 0x36, 0x00, 0x00},
    // 0x43 C
    { 0x00, 0x3c, 0x42, 0x42, 0x42, 0x3c, 0x00, 0x00, 0x00, 0x7e, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00},
    // 0x44 D
    { 0x00, 0x7c, 0x42, 0x42, 0x42, 0x7c, 0x00, 0x00, 0x00, 0x7f, 0x41, 0x41, 0x41, 0x3e, 0x00, 0x00},
    // 0
}

欢迎关注我的其它发布渠道