Comments 24
Тут идут слова благодарности конторе Espressif с ее серией микроконтроллеров с Wi-Fi. Никогда еще не было так легко обновить прошивку по воздуху. Никаких проводов, разъемов, гальванических развязок, USB-to-serial переходников, настроек скорости последовательного порта, командировок. Даже софта FW_loader не надо! В вебморде выбрал прошивку с рабочего стола ноутбука, из теплого офиса с удобным креслом за тысячу километров от железки, и, с чувством достоинства, нажал кнопку "Загрузить". Поработал и получил денег кучу.
Не совсем понятно для кого эти требования подходят. В большинстве случаев удобнее всего сделать обёртку над фирменными утилитами, например STM32CubeProgrammerCLI или esptool. Ну и тут уже половина требований можно считать выполненными, т.к. они реализованы в них.
6--Программа FW_Loader должна быть собрана на языке программирования Си.
Питон + pyinstaller / bincopy / serial и argparse решают задачу. Если надо добавь шифрование, также решается добавлением библиотеки. Также питон однопоточный.
Я же написал почему на Си. Потому что прошивка пишется на Си. Протокол одинаковый. Значит 70 % исходников общие.
Ну сами подумайте, кто программирует микроконтроллеры на python?
Я понимаю, что Вам хочется за счёт embedded компании выучить python , а потом пойти на python сайты программировать. Успехов!
Я понимаю . У Вас IDE IAR не может exe скомпилировать. Вот и приходится менять язык для desktop разработки.
Поддержу, не вижу особого смысла писать такого рода утилиты на С. Python проще, а главное, оброс массой простых в использовании библиотек на все случаи жизни. Просто приведу примеры: у нас в ходу загрузчики, общающиеся через целый зоопарк интерфейсов: UART, USB, BLE, CAN, UDS-DoIP-Ethernet. Для всех этих интерфейсов нашлись достаточно удобные кросс-платформенные Python библиотеки, благодаря которым написание загрузчиков ощущалось как отдых от основной деятельности, какой-нибудь CAN на приём поднялся вообще тремя строчками итд. Сколько времени/сил отняло бы завести сами эти интерфейсы под две ОС на С даже думать не хочется, невозможность переиспользовать Сшную реализацию своих протоколов - мелочи в сравнении с этим. А уж со всякой «косметикой» вроде именованных параметров командной строки, которые автор статьи аж в отдельный пункт TODO вынес, или разноцветным выводом в консоль, на Python вопросов вообще не возникает.
В embedded вообще хватает хардкора, так пусть он будет только там, где оправдан. Поверхностные знания Python, которых хватит для написания подобного рода утилит, достаются довольно «дёшево».
Я даже больше скажу.
Вот прямо сейчас я сделал на Python загрузчик. Буквально за пять минут и через две итерации:

Он реально работает!
Только что проверил на своем дивайсе. Конвертирует HEX в бинарник и загружает через FTP сервер реализованый в моем модуле.
А вот весь текст скрипта:
Скрытый текст
import os
import ftplib
import tempfile
import sys
from intelhex import IntelHex
import wx
def convert_hex_to_bin(hex_path, bin_path):
"""
Convert Intel HEX file to a binary file using IntelHex library.
"""
ih = IntelHex()
ih.loadhex(hex_path)
# Write binary file; fill gaps with 0xFF
# The tobinfile signature is (filename, start=None, end=None, pad=0xFF)
ih.tobinfile(bin_path, pad=0xFF)
class FTPUploaderFrame(wx.Frame):
def __init__(self, parent=None, title="FTP HEX->BIN Uploader"):
super().__init__(parent, title=title, size=(500, 400))
panel = wx.Panel(self)
vbox = wx.BoxSizer(wx.VERTICAL)
# FTP Connection
gb_conn = wx.StaticBox(panel, label="FTP Connection")
sbs_conn = wx.StaticBoxSizer(gb_conn, wx.VERTICAL)
fgs = wx.FlexGridSizer(4, 2, 10, 10)
fgs.AddGrowableCol(1, 1)
fgs.Add(wx.StaticText(panel, label="Host:"), 0, wx.ALIGN_CENTER_VERTICAL)
self.host_txt = wx.TextCtrl(panel, value="192.168.16.137")
fgs.Add(self.host_txt, 1, wx.EXPAND)
fgs.Add(wx.StaticText(panel, label="Port:"), 0, wx.ALIGN_CENTER_VERTICAL)
self.port_txt = wx.TextCtrl(panel, value="21")
fgs.Add(self.port_txt, 1, wx.EXPAND)
fgs.Add(wx.StaticText(panel, label="Username:"), 0, wx.ALIGN_CENTER_VERTICAL)
self.user_txt = wx.TextCtrl(panel, value="ftp_login")
fgs.Add(self.user_txt, 1, wx.EXPAND)
fgs.Add(wx.StaticText(panel, label="Password:"), 0, wx.ALIGN_CENTER_VERTICAL)
self.pass_txt = wx.TextCtrl(panel, value="pass", style=wx.TE_PASSWORD)
fgs.Add(self.pass_txt, 1, wx.EXPAND)
sbs_conn.Add(fgs, 1, wx.ALL|wx.EXPAND, 10)
vbox.Add(sbs_conn, 0, wx.ALL|wx.EXPAND, 10)
# HEX File Selection
gb_file = wx.StaticBox(panel, label="HEX File")
sbs_file = wx.StaticBoxSizer(gb_file, wx.HORIZONTAL)
self.file_txt = wx.TextCtrl(panel, value="Test.hex", style=wx.TE_READONLY)
file_btn = wx.Button(panel, label="Browse...")
file_btn.Bind(wx.EVT_BUTTON, self.on_browse)
sbs_file.Add(self.file_txt, 1, wx.ALL|wx.EXPAND, 5)
sbs_file.Add(file_btn, 0, wx.ALL, 5)
vbox.Add(sbs_file, 0, wx.LEFT|wx.RIGHT|wx.EXPAND, 10)
# Progress Gauge
self.gauge = wx.Gauge(panel, range=100, size=(450, 25))
vbox.Add(self.gauge, 0, wx.ALL|wx.CENTER, 10)
# Upload Button
upload_btn = wx.Button(panel, label="Convert & Upload")
upload_btn.Bind(wx.EVT_BUTTON, self.on_upload)
vbox.Add(upload_btn, 0, wx.ALL|wx.CENTER, 10)
panel.SetSizer(vbox)
self.Centre()
def on_browse(self, event):
with wx.FileDialog(
self,
"Select HEX file",
wildcard="HEX files (*.hex)|*.hex|All files (*.*)|*.*",
style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST
) as dlg:
if dlg.ShowModal() == wx.ID_OK:
path = dlg.GetPath()
self.file_txt.SetValue(path)
def on_upload(self, event):
host = self.host_txt.GetValue().strip()
port_str = self.port_txt.GetValue().strip()
username = self.user_txt.GetValue().strip()
password = self.pass_txt.GetValue().strip()
hex_path = self.file_txt.GetValue()
if not host or not hex_path:
wx.MessageBox(
"Please specify both FTP host and HEX file.",
"Error",
wx.OK | wx.ICON_ERROR
)
return
try:
port = int(port_str)
except ValueError:
wx.MessageBox(
"Port must be a number.",
"Error",
wx.OK | wx.ICON_ERROR
)
return
bin_path = os.path.splitext(hex_path)[0] + ".bin"
try:
convert_hex_to_bin(hex_path, bin_path)
except Exception as e:
wx.MessageBox(
f"Failed to convert HEX to binary:\n{e}",
"Conversion Failed",
wx.OK | wx.ICON_ERROR
)
return
try:
filesize = os.path.getsize(bin_path)
uploaded = 0
import time
start_time = time.time()
def callback(data):
nonlocal uploaded
uploaded += len(data)
percent = int(uploaded / filesize * 100)
wx.CallAfter(self.gauge.SetValue, percent)
with ftplib.FTP() as ftp:
ftp.connect(host, port)
ftp.login(username, password)
with open(bin_path, 'rb') as f:
ftp.storbinary(
f'STOR {os.path.basename(bin_path)}',
f,
blocksize=8192,
callback=callback
)
elapsed = time.time() - start_time
speed = filesize / elapsed if elapsed > 0 else 0
# Show dialog with bytes and speed
dlg = wx.MessageDialog(
self,
f"Uploaded {filesize} bytes in {elapsed:.2f} s\nSpeed: {speed:.1f} bytes/sec",
"Upload Complete",
wx.OK
)
dlg.SetOKLabel("Close")
dlg.ShowModal()
dlg.Destroy()
self.gauge.SetValue(0)
except Exception as e:
wx.MessageBox(
f"FTP upload error:\n{e}",
"Upload Failed",
wx.OK | wx.ICON_ERROR
)
return
# Optionally cleanup
# os.remove(bin_path)
def main():
app = wx.App(False)
frame = FTPUploaderFrame()
frame.Show()
app.MainLoop()
# Unit tests for conversion
import unittest
class TestHexConversion(unittest.TestCase):
def test_simple_conversion(self):
# Create a minimal Intel HEX file with 2 lines of data (16 bytes each)
content = (
":020000040000FA\n"
":100000000C9423000C944E000C944E000C944E00A6\n"
":00000001FF\n"
)
with tempfile.NamedTemporaryFile('w+', suffix='.hex', delete=False) as tmp_hex:
tmp_hex.write(content)
tmp_hex.flush()
bin_path = tmp_hex.name.replace('.hex', '.bin')
convert_hex_to_bin(tmp_hex.name, bin_path)
self.assertTrue(os.path.exists(bin_path))
size = os.path.getsize(bin_path)
# Expect 16 bytes of data
self.assertEqual(size, 16)
os.remove(bin_path)
os.remove(tmp_hex.name)
def test_conversion_with_empty_file(self):
# Create an empty HEX file should result in 0-byte binary
with tempfile.NamedTemporaryFile('w+', suffix='.hex', delete=False) as tmp_hex:
tmp_hex.write("")
tmp_hex.flush()
bin_path = tmp_hex.name.replace('.hex', '.bin')
convert_hex_to_bin(tmp_hex.name, bin_path)
self.assertTrue(os.path.exists(bin_path))
size = os.path.getsize(bin_path)
self.assertEqual(size, 0)
os.remove(bin_path)
os.remove(tmp_hex.name)
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == '--test':
unittest.main(module=__name__, argv=sys.argv[:1])
else:
main()
Даже юнит-тест прикручен!
Тему с загрузчиками можно закрывать полностью
Десктоп утилиты для пршивки - плохая идея. Даже писать приложение для смартфона плохая идея. Придется заниматься их поддержкой и постоянным переносом на новые апгрейды OS и тестированием. Но не для того делают загрузчики чтобы получить головную боль.
Что важнее для инфраструктуры загрузчика - это кастомная упаковка, надежная шифрация и гибкость способов доставки.
Даже WEB интерфейс не лучший вариант. Лучше иметь просто открытый WEB API для загрузчика. А уж "морду" вам нарисует ChatGPT какую хотите и итегрирует в систему заказчика. Никаких артефактов производителя чипов не будет.
Вот в этом загрузчике доставка прошивки сделана даже по MQTT протоколу через любого MQTT брокера, даже самого примитивного. Это в добавок к FTP, HTTP, и просто USB Mass Storage Class, и в крайнем случае можно передать через SD карту с аппаратным паролем.
Решение с аргументами в командной строке утилиты тоже уже архаичное.
ИМХО, но лучшее решение - конфигурационный файл. Как в утилите упаковщика из проекта этого загрузчика.
Это гораздо мощнее и гибче.
А вот сам конфигурационный файл вы можете создавать хоть на Python хоть на Bash.
На это есть GitHub Copilot, который вам наваяет любых сборщиков конфигов за секунды.
Python и Bash знать при этом не нужно. Copilot пишет код на на них с первого раза и без ошибок. Потому что он умеет их сам компилировать, запускать и перепроверять.
В программируемом девайсе должен быть USB-COM который в windows не требует специальных драйверов, а лоадер должен сам находить номер порта куда подключен программируемый девайс. В этом основной затык у не очень продвинутых юзеров.
Как вариант в windows девайс обнаруживается как внешний диск, на который надо просто скопировать файл прошивки, без всяких специальных лоадеров. Но это для жирных процессоров.
Почти все популярные фреймворки для микроконтроллеров на сегодня имеют RNDIS.
Windows 11 подхватывает RNDIS также без всяких бубнов с драйверами.
Но при этом имеет и загрузку через FTP более чем удобную и стандартную, и тот же терминал что и через COM порт, и сам порт искать не надо, и еще с десяток сервисов.
А с USB-COM вы определенно лишаете себя кучи удовольствий.
Не часто прочитаешь слово "фреймворк" и "микроконтроллер "в одном предложении.
Что такое RNDIS?
Но зачем?
Если используется usb, со стороны МК в бутлоадере прикинуться mass storage'м и со стороны ПК тупо скопировать туда файл прошивки без каких-либо дополнительных утилит.
Да. Либо на sd карту, подключенную к spi прописывать hex.
Какая блоха SD? Какой заяц SPI? Почему HEX?
Насколько знаю никто текстовый формат непосредственно в железку передавать не будет. Не исключаю что всегда возможно найдутся альтернативно одарённые, но если посмотреть на встроенные бутромы различных МК все обычно городят свой примитивный протокол (бинарный!) с указанием адреса/размера и контрольной суммы куда что писать во внутреннюю (и не очень) память.
Есть даже некие попытки как-то стандартизовать это, например, мелкомягкий .UF2 (в rp2040)
А весь описанный функционал можно спокойно перенести внутрь МК, а со стороны ПК "идеальным loaderом" становится консольная команда copy.
Желательно, чтобы фрагменты прошивки передавались в сжатом виде. Хотя бы в кодировке BASE64
Интересно, с каких это пор бейз64 вдруг стал "сжатым" видом.
Хороший вопрос. Посмотрите сами
https://cryptii.com/pipes/hex-to-base64
В пределе, по мере увеличения длинны массива записи на BASE64 занимают меньше символов
0000000000 (10)
AAAAAAA= (8)
FFFFFFFF
/////w==
0c9e0c74a68721ee (16)
DJ4MdKaHIe4= (12)
Вот и получается, что BASE64 - это такое паллиативное сжатие.
Простите, что?
BASE64 как раз таки УВЕЛИЧИВАЕТ объем передаваемых данных - вместо 3 байт бинарника получается 4 байта транспортного протокола.
Ибо передавать в слабенький МК с ограниченной RAM что-то, отличное от бинарника - это как вёдрами носить бетон на стройке небоскреба (уж пардон за ассоциацию). Все иные форматы должны конвертироваться в *.bin самим FW_Loader, и только потом загружаться в МК.
P.S.
Но само использование BASE64 я поддерживаю на 146%.
Ибо этот протокол заведомо пролазит через любые настройки и аппаратные реализации COM-портов, включая удаленные соединения через Modbus/ASCII, конвертеры Ethernet/COM с кривой поддержкой Xon/Xoff и 7-битные радиомодемы. То есть, благодаря ему можно создавать загрузчик(и) через любую среду передачи от "большого" компа, имея один коротенький бутлоадер в МК.
О, да. А если в текстовый файл писать бинарное, а не шестнадцатеричное представление, то сжатие окажется ещё в 4 раза выше! Крутота!
Вообще, если скармливается именно контроллеру и именно гекс, и при этом надо его ужимать для скорости, это почти наверняка говорит о том, что формат выбран неверно (нужно разве что для чего-то из экзотики/антиквариата, как упомянуто выше). А для утилиты, отсылающей прошивку в девайс, пофиг, сжат гекс или нет. Скорости ощутимо не добавится.
Удивительно, что никто не написал, что в утилите FW_Loader должен быть progress bar. Индикатор прогресса.
Чтобы оператор мог непрерывно оценивать сколько ему еще ждать окончания обновления прошивки. Нет ничего мучительнее, чем неизвестность.
Только сейчас про него вспомнил.
Атрибуты Хорошего Loader-a