找羊开源加热台软硬件讲解
目录
前言
我最近复刻了找羊的加热台项目,前前后后经过了大半个月,也花了几天时间学习固件源码。现在编写一篇笔记,为此项目画上句号。(作者是新人,若有理解错误的地方,欢迎指正)
一、硬件
主控选用ESP-12F,PCB板上集成有CH340C的串口转换电路,可以选择TypeC口烧录,也可以选择排母烧录
1. 电源与供电模块
这部分由三部分组成,分别是 Type-C 接口、LDO 稳压、5V 电源模块。 Type-C接口提供5V电压,为主控芯片、ch340cUSB转ttl串口电路、屏幕、编码器、等等供电(即电路板的下半部分);LDO稳压电路将输入的5V电压转化为3.3V,供电给ESP-12F、屏幕、编码器等工作电压为3.3V的模块。以上两个供电模块负责烧录时到接入市电前的测试(务必确保正常再接入市电)。5V电源模块将市电220V转化为5V输出,为 LDO、继电器驱动等电路供电。
2.ch340c串口通信
CH340C 是 USB 转串口芯片,实现 电脑 USB - 串口 - ESP8266 的数据传输。注意与 ESP8266 的RX/TX交叉连接,烧录时要把芯片的GPIO0拉低电平(接地芯片进入下载模式),烧录完要断开。
3.主控模块
RST:复位引脚,通过 R54(10K)上拉、C22(1uF)组成复位电路,实现上电复位。
TXD0/RXD0:串口通信引脚
GPIO0-GPIO16:通用输入输出引脚,用于连接传感器、按键、控制外设等
ADC:模拟 - 数字转换引脚,连接温度传感器(NTC)的输出,实现温度采集
4.加热控制模块
用于控制加热板的通断,实现温度调节
光耦与继电器:MOC3041将 ESP8266 的 PWM 信号隔离后,驱动 BT136-600E 双向可控硅。R46(100Ω)、AO3400组成 PWM 驱动电路,控制光耦的导通;R47(100Ω)限制光耦电流。
可控硅与加热板:BT136-600E 导通后,为 加热板供电。R49(390Ω)、R50(390Ω)是可控硅的保护电阻;C19(10nF)是阻容吸收电路,防止电源波动对可控硅的冲击。
5.温度采集模块
NTC 温度传感器: 热敏电阻通过 R40(10K)组成分压电路,将温度变化转换为电压变化,再由 ESP8266 的ADC引脚采集,实现温度检测
风扇控制:AO3400由 ESP8266 的GPIO15引脚控制,实现风扇的通断。1N4148是续流二极管,防止风扇电机的反向电动势损坏 MOS 管。
6.人机交互模块
OLED 显示:使用0.91寸OLED ,SDA(GPIO13)、SCL(GPIO12)与 ESP8266 通信,实现温度、状态等信息的显示。
按键与旋钮:使用EC11 旋钮,SWB(GPIO0)、SWA(GPIO2)、SW(GPIO4)实现功能选择、参数调节等操作。
二、软件
由于本人编程语言学得十分浅薄,若有不对的地方望各位大佬指正。
1.启动与主循环
setup() 初始化 OLED、加载 EEPROM、开启定时器、联网与旋钮;loop() 只调 UI、EEPROM 异步写和 MIOT 任务,CPU 频率维持 160MHz 保证性能。
void setup()
{
delay(100);
#ifdef DEBUG
{
Serial.begin(115200);
}
#endif
oled.begin();
eeprom.read_all_data();
Ticker_init();
miot.begin();
ec11.begin(5, 2, 4, ui_key_callb);
ec11.speed_up(true);
ec11.speed_up_max(20);
ui.page_switch_flg = true;
}
void loop()
{
delay(1);
if (system_get_cpu_freq() != SYS_CPU_160MHZ)
system_update_cpu_freq(SYS_CPU_160MHZ);
ui.run_task();
eeprom.write_task();
miot.run_task();
}
![]()
所有重活不进主循环,而是交给定时器和任务状态机,这是嵌入式实时项目常见结构。
2.定时器与任务节拍
Ticker_init() 绑定 1s/100ms/50ms/25ms 定时任务:1s 节拍里完成温度采样、PID 调节、回流阶段推进、恒温倒计时;50ms 轮换两路 ADC;25ms 驱动 OLED 刷新;100ms 负责 EEPROM 写入延迟与 UI 状态恢复。
void Ticker_init()
{
ticker25ms.attach_ms(25,ms25_tic);
ticker100_ms.attach_ms(100,ms100_tic);
ticker50_ms.attach_ms(50,ms50_tic);
ticker1s.attach(1,s1_tic);
}
![]()
void s1_tic()
{
adc.get_temp_task();
pwm.temp_set();
...
if(pwm.temp_reached_flg == true)
{
if(min_count == 60)
{
min_count = 0;
temp_time_buf --;
}
}
...
if(pwm.backflow_working_state == 1)
{
tmp = (curve_temp_buf[0] - 39) * 10 / 25 ;
...
pwm.percent = (adc.now_temp - 38) * 10 / tmp;
...
}
else if(pwm.backflow_working_state == 2)
{
...
}
![]()
3.温度采样与校准
- 利用 switch_io 控制模拟前端在低/高温两路之间切换,双 FIFO 平滑 ADC 值。
- 低温通道用于室温至 ~160℃,高温通道覆盖 150℃ 以上;两者通过 adc_error 进行偏差校正。
- 支持自动/手动校准:UI “温度校准”页触发全功率加热,记录 adc_max_temp 并写入 EEPROM。
void ADC::get()
{
uint8_t i;
if (adc_mode_state == channel_low_temp)
{
for (i = 0; i < 7; i++)
adc_buf[i] = adc_buf[i + 1];
adc_buf[i] = analogRead(A0);
}
else
{
for (i = 0; i < 7; i++)
adc_buf_high[i] = adc_buf_high[i + 1];
adc_buf_high[i] = analogRead(A0);
}
set_channel(!adc_mode_state);
}
![]()
rt = vol_low * 1000 / ((3300 - vol_low) / 13);
now_temp = (100 / (log(rt / 10000.0) / 3950 + 1 / 298.15) - 27315) / 100;
if (now_temp < 151)
return;
if (now_temp < 160 && !adc_error)
{
if (pwm.power || adc_max_temp_auto_flg == 0)
temp_buf = now_temp;
}
vol_high = vol_high * 1000 / 21;
buf = vol_high * 1000 / ((3300000 - vol_high) / 13.0);
tt = (100 / (log(buf / 10000) / 3950 + 1 / 298.15) - 27315) / 100;
if (temp_buf)
adc_error = temp_buf - tt;
if (adc_max_temp_auto_flg)
{
int16_t tmp = 150 - adc_error;
rt = hotbed_max_temp - adc_error;
int16_t tmp1 = adc_max_temp - rt;
buf = double(tmp1) / (double)(adc_max_temp - tmp);
tt = (double)tt - ((double)(tt - tmp) * buf);
}
if (adc_max_temp_auto_flg)
now_temp = tt + adc_error;
else
now_temp = tt;
}
![]()
4.PWM 输出与温控控制律
- 4ms 中断生成软件 PWM,占空值由 high_time 控制;IO14 驱动加热,IO15 控风扇。
- 控制策略类似 PID:kp/ki/kd + 自适应积分限幅 + 误差分段降速,低温/高温段参数不同;另有“接近目标时减速”“误差为负则清零输出”等保护。
- 恒温模式:达到温度后进入倒计时,时间到自动 end();回流模式按 curve_temp_buf 定义的温度-时间表推进。
void PWM::temp_set()
{
if (!pwm.power)
{
high_time = 0;
return;
}
if (temp_mode == Co_Temp)
{
need_set_temp = temp_buf;
if (adc.now_temp >= temp_buf && temp_reached_flg == false)
{
if (ui.temp_time_switch_flg == false)
ui.temp_time_switch_flg = true;
}
if (temp_reached_flg == true && temp_time_buf == 0)
{
end();
return;
}
}
else
need_set_temp = backflow_temp_tmp;
ek[2] = ek[1];
ek[1] = ek[0];
ek[0] = need_set_temp - adc.now_temp;
if (adc.now_temp == 38)
{
pwm_buf = need_set_temp;
}
else
{
pwm_buf_f += ek[0];
if (adc.now_temp < 180)
{
if (pwm_buf_f > 20)
pwm_buf_f = 20;
else if (pwm_buf_f < -20)
pwm_buf_f = -20;
}
else
{
if (pwm_buf_f > 50)
pwm_buf_f = 50;
else if (pwm_buf_f < -50)
pwm_buf_f = -50;
}
if (adc.now_temp < 200)
pwm_buf = kp * (ek[0] + ki * pwm_buf_f + kd * (ek[0] - ek[1]));
else
pwm_buf = kp * (ek[0] + ki_high * pwm_buf_f + kd * (ek[0] - ek[1]));
if (ek[0] < 10 && ek[0] > 3)
{
if (adc.now_temp < 200)
pwm_buf /= 3;
}
else if (ek[0] < 4)
{
if (adc.now_temp < 200)
{
pwm_buf /= 8;
}
else
pwm_buf /= 3;
if (ek[0] > -1)
pwm_buf += need_set_temp / 10;
}
if (ek[0] < 0 && pwm_buf > 0)
{
pwm_buf = 0;
}
}
if (pwm_buf < 0)
{
pwm_buf = 0;
}
if (pwm_buf > 255)
{
pwm_buf = 255;
}
high_time = pwm_buf;
Serial.println(high_time);
}
![]()
5.UI 与交互设计
- 三层结构:主页(实时温度/模式/加热动画)、菜单页(八个功能)、设置页(根据菜单进入不同子界面)。
- UI::run_task() 处理唤醒/休眠、旋钮事件、菜单导航、OLED 重绘。
- 旋钮(EC11)支持单击/双击/长按 + 旋转加速,通过中断 + 1ms 定时器消抖。
void UI::run_task()
{
wake_sleep_page();
if (oled_sleep_flg)
return;
switch (page_num)
{
case 1:
temp_move();
temp_mode_move();
heat_move();
temp_time_switch();
if(show_warning_flg)
{
show_warning();
show_warning_flg = 0;
}
break;
case 2:
page2_move();
break;
case 3:
page3_switch();
blinker_config();
error_temp_fix_page_move();
break;
}
if (page2_move_flg)
return;
page_switch(switch_buf);
if (!oled_flg)
return;
oled_flg = 0;
oled.clr();
show_page(0, 0, page_num);
write_oled_light();
oled.refresh();
}
![]()
6.EEPROM 配置存取
- eeprom_flash::read_all_data() 上电读取:预设温度、模式、恒温时长、屏幕亮度、回流曲线、Blinker ID、最大温度限制等;若 first_download_add 标记不为 0,则调用 data_init() 写默认值。
- 写入采用延迟策略:当 write_flg 置位后,ms100_tic() 计数 2s,再调用 write_task();写入前关闭编码器中断,防止 EEPROM 操作影响旋钮。
void eeprom_flash::data_init()
{
pwm.temp_buf = 128;
pwm.temp_mode = 1;
miot.miot_able = 0;
pwm.temp_mode1_time = 10;
ui.oled_light = 100;
adc.adc_max_temp = 255;
adc.hotbed_max_temp = 255;
write_flg = 2;
for (int x = first_download_add; x < eeprom_size; x++)
EEPROM.write(x, 0);
}
void eeprom_flash::read_all_data()
{
ec11.int_close();
EEPROM.begin(eeprom_size);
if (EEPROM.read(first_download_add))
{
data_init();
}
else
{
pwm.temp_buf = ((uint16_t)(EEPROM.read(set_temp_high_add) << 8)) | EEPROM.read(set_temp_low_add);
pwm.temp_mode = EEPROM.read(set_temp_mode_add);
miot.miot_able = EEPROM.read(miot_able_add);
pwm.temp_mode1_time = ((uint16_t)(EEPROM.read(temp_mode1_time_high_add) << 8)) | EEPROM.read(temp_mode1_time_low_add);
ui.oled_light = EEPROM.read(set_oled_light_add);
curve_temp_buf[0] = (EEPROM.read(temp0_data_start_add) * 256) + EEPROM.read(temp0_data_start_add + 1);
curve_temp_buf[1] = (EEPROM.read(temp0_data_start_add + 2) * 256) + EEPROM.read(temp0_data_start_add + 3);
curve_temp_buf[2] = (EEPROM.read(temp0_data_start_add + 4) * 256) + EEPROM.read(temp0_data_start_add + 5);
curve_temp_buf[3] = (EEPROM.read(temp0_data_start_add + 6) * 256) + EEPROM.read(temp0_data_start_add + 7);
char tmp;
for (int x = blinker_id_add; x < eeprom_size; x++)
{
tmp = EEPROM.read(x);
if (tmp != 0)
wifima.blinker_id += tmp;
else
break;
}
Serial.print("Blinker ID:");
Serial.println(wifima.blinker_id);
adc.hotbed_max_temp = ((uint16_t)EEPROM.read(hotbed_max_temp_high_add) << 8) | EEPROM.read(hotbed_max_temp_low_add);
if (adc.hotbed_max_temp > 270 || adc.hotbed_max_temp < 240)
{
adc.hotbed_max_temp = 255;
EEPROM.write(hotbed_max_temp_low_add, adc.hotbed_max_temp & 0xff);
EEPROM.write(hotbed_max_temp_high_add, adc.hotbed_max_temp >> 8);
}
adc.adc_max_temp = ((uint16_t)EEPROM.read(adc_max_temp_high_add) << 8) | EEPROM.read(adc_max_temp_low_add);
if (adc.adc_max_temp > 400 || adc.adc_max_temp < 190)
{
adc.adc_max_temp = 255;
EEPROM.write(adc_max_temp_low_add, adc.adc_max_temp & 0xff);
EEPROM.write(adc_max_temp_high_add, adc.adc_max_temp >> 8);
}
}
EEPROM.commit();
EEPROM.end();
ec11.int_work();
oled.light(ui.oled_light);
}
![]()
7.配网与云端配网与云端
- AP 配网:WiFiManager::startConfigPortal() 打开 QF_HP 热点 + DNS 劫持,提供网页输入 WiFi/密钥,成功后写 EEPROM 并重启。
- 启动 WiFi:set_wifi::power_on_conect() 负责上电尝试连接保存的 WiFi,并在 OLED 上做动画提示。
- Blinker + MIOT:MIOT::begin() 在启用情况下连接 WiFi、配置按钮/滑块回调,映射 App 控件与“小爱模式”;run_task() 在循环里调用 Blinker.run()。
bool WiFiManager::startConfigPortal(char const *apName)
{
WiFi.persistent(true);
WiFi.mode(WIFI_AP_STA);
data_get_flg = false;
wifima_flg = 1;
back_flg = 0;
WiFi.softAP(apName);
dnsServer.start(DNS_PORT, "*", WiFi.softAPIP());
server.on("/", HTTP_GET, handleRoot);
server.onNotFound(handleRoot);
server.on("/Handledata", HTTP_GET, Handledata);
server.begin();
for (;;)
{
server.handleClient();
dnsServer.processNextRequest();
if (back_flg)
{
back_flg = 0;
server.close();
WiFi.mode(WIFI_STA);
wifima_flg = 0;
return 0;
}
if (data_get_flg)
{
bool wifi_sta = 0;
if (_ssid != "")
{
Serial.println("Conect to WiFi:");
Serial.println(_ssid);
Serial.println(_pass);
WiFi.begin(_ssid.c_str(), _pass.c_str());
uint8_t tmp = 20;
while (tmp--)
{
delay(100);
if (WiFi.status() == WL_CONNECTED)
{
wifi_sta = 1;
break;
}
}
}
ec11.int_close();
if (strlen(wifima.blinker_id.c_str()) == 12)
{
Serial.println("Blinker ID:");
Serial.println(wifima.blinker_id);
Serial.println(miot.miot_able);
eeprom.write_str(blinker_id_add, wifima.blinker_id);
if (WiFi.SSID() != "")
{
if (miot.miot_able)
eeprom.write(miot_able_add, 1);
ESP.reset();
}
}
ec11.int_work();
server.close();
WiFi.mode(WIFI_STA);
wifima_flg = 0;
return 1;
}
![]()
void MIOT::begin()
{
if (miot_able)
{
open_flg = 1;
if (WiFi.status() != WL_CONNECTED)
{
wifi_conect_flg = setwifi.power_on_conect();
if (!wifi_conect_flg)
{
Serial.println("connect wifi error!");
return;
}
Serial.println("connect wifi ok!");
}
}
else
return;
if (strlen(&wifima.blinker_id[0]) == 12)
{
Serial.println("blinker start!");
Blinker.begin(&wifima.blinker_id[0],(const char*)&WiFi.SSID()[0],(const char*)&WiFi.psk()[0]);
}
else
{
Serial.println("blinker id error!");
return;
}
#ifdef DEBUG
{
BLINKER_DEBUG.stream(Serial);
}
#else
{
const char *p = &wifima.blinker_id[0];
Blinker.begin(p);
}
#endif
Button1.attach(button1_callback);
Button2.attach(button2_callback);
Button3.attach(button3_callback);
Button4.attach(button4_callback);
Button5.attach(button5_callback);
BlinkerMIOT.attachPowerState(miotPowerState);
BlinkerMIOT.attachColor(miotColor);
Slider1.attach(slider1_callback);
BlinkerMIOT.attachMode(miotMode);
}
void MIOT::run_task()
{
![]()
总结
想必大家也看出来,我软件部分用了ai,这也是没办法的事,固件是用C++写的,对于只学过C语言且C语言也学得不怎么样的作者来说,要解说它未免太为难了。好了,通过这个项目,我最大的提升是焊接能力和EDA的熟练度,当然原理图和相关元器件的特性也是了解了很多,唯独软件代码部分感觉提升相对有限。
作者昵称:墨然循迹
原文链接:https://blog.csdn.net/2301_80116556/article/details/154793649
作者主页:https://blog.csdn.net/2301_80116556