找羊开源加热台软硬件讲解
目录
- 前言
- 硬件
- 软件
总结
前言
我最近复刻了找羊的加热台项目,前前后后经过了大半个月,也花了几天时间学习固件源码。现在编写一篇笔记,为此项目画上句号。(作者是新人,若有理解错误的地方,欢迎指正)
一、硬件
主控选用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; // Serial.print("low:"); // Serial.println(now_temp); 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; // Serial.print("high:"); // Serial.println(tt); 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() { // EEPROM初始化数据 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.setAutoReconnect(false); // if (!WiFi.isConnected()) // { // WiFi.disconnect(); // this alone is not enough to stop the autoconnecter // } WiFi.persistent(true); WiFi.mode(WIFI_AP_STA); data_get_flg = false; wifima_flg = 1; back_flg = 0; WiFi.softAP(apName); //生成wifi的名称 dnsServer.start(DNS_PORT, "*", WiFi.softAPIP()); //初始化DNS服务器dnsServer.start(DNS_PORT, "*", apIP) server.on("/", HTTP_GET, handleRoot); //主页回调函数(发送HTML到客户端) server.onNotFound(handleRoot); //设置无法响应的http请求的回调函数(失败需要跳到主页否则强制门户不能实现) server.on("/Handledata", HTTP_GET, Handledata); //获取用户输入的数据进行处理 server.begin(); //开启服务器 for (;;) { server.handleClient(); // HTTP请求处理 dnsServer.processNextRequest(); // DNS请求处理 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的熟练度,当然原理图和相关元器件的特性也是了解了很多,唯独软件代码部分感觉提升相对有限