В процессе серийного производства электронных устройств следующим этапом после монтажа печатных плат является стенд серийной прошивки и тестирования. На нем производится прошивка микроконтроллера/микропроцессора, запуск платы и ее проверка в том или ином объеме. Естественно, как и монтаж плат, работа стенда также должна быть мах автоматизирована. В крайнем случае, ручные операции, от которых не удалось избавиться, должны быть предельно просты, интуитивно понятны и монотонны. В статье описывается процесс разработки подобного стенда под микропроцессор IMX6ULL.
В конце приводится ссылка на исходный код стенда, который может быть адаптирован под другие семейства микропроцессоров с архитектурой ARM.
Итак, в процессе работы с отладочной платой мы более-менее убедились в том, что выбранный микропроцессор нас более-менее устраивает, - нужные интерфейсы поддерживаются, быстродействия хватает и появилось понимание примерного размера финальной прошивки. Останавливаем свой выбор на данном процессоре. Пора приступать к разработке печатной платы.
Среди прочих требований к плате нужно продумать решение трех вопросов:
где будет храниться образ ядра Linux (прошивка, image): SPI флеш, NAND флеш, microSD карта или еще что-либо;
как эта прошивка будет поставляться в серийно выпускаемые изделия;
как будет задаваться загрузочный интерфейс процессора;
Варианты ответов на каждый вопрос:
Проще всего использовать microSD карту. В этом случае вам нужно лишь подготовить N-ное количество microSD карт с образом ядра вставить их во все изготовленные устройства. Но такой способ может не подойти, если:
необходимо разработать устройство в индустриальном исполнении (рабочие температуры от -40 С);
вы хотите осложнить доступ к прошивке устройства;
устройство может подвергаться вибрациям;
у вас есть строгие ограничения по себестоимости конечного изделия и каждый цент на счету (microSD карта + разъем заведомо дороже, чем микросхема флеш-памяти распаянная на плате);
у вас есть строгие ограничения по габаритам конечного изделия;
Если microSD-карта не подходит, - остается выбор между NOR и NAND памятью. В целом, при выборе типа памяти нужно отталкиваться от размера финальной прошивки. Если до 32 Мб, - то лучше выбрать микросхему памяти NOR (цена - меньше, ресурс - выше), если больше 32 Мб - то остается NAND.
Если выбор пал на микросхему памяти, распаянную на плате, то есть два варианта:
образ ядра прошивается в микросхему до распайки на плате. В этом случае микросхемы по-очередно извлекаются из катушки, устанавливаются в программатор (наподобие такого), затем упаковываются обратно в катушку. Очень трудоемкий и не технологичный процесс.
образ ядра прошивается в микросхему после распайки на плате с помощью микропроцессора. Процессор нужно каким-либо образом запустить (при этом загрузочная микросхема флеш памяти пуста!), доставить ему образ прошивки, которую процессор и загрузит в м/сх памяти. Звучит как нечто, что очень сложное в реализации, зато это снижает количество ручных операций в процессе производства. Забегая вперед скажу, что мы выбрали именно этот вариант и нам удалось его автоматизировать.
При работе с отладочными платами вы почти наверняка встречали нечто подобное:
![](https://habrastorage.org/getpro/habr/upload_files/856/a00/247/856a002471a2ba9fbc9366e3e9361bb5.jpg)
переключатель, задающий загрузочный режим. Дело в том, что процессоры, как правило, поддерживают несколько различных вариантов загрузки, таких как SPI, QSPI, SDHC, UART и пр. Какой-именно вариант загрузки использовать определяется фьюз-битами. Процессор может вычитать фьюз-биты из регистров, либо определив состояние определенных GPIO. Какой именно способ процессору использовать, в свою очередь, задается состоянием двух других ножек (BOOT_MODE0 и BOOT_MODE1).
Также можно встретить вариант одного-единственного варианта загрузки, заданного резисторами:
Если габариты позволяют и не жалко несколько ножек процессора, то лучше выбрать один из этих вариантов (переключатель или резисторы).
В случае же, если вы ограничены в габаритах (наш случай) или нужно высвободить ножки процессора под другие задачи, то остается только третий вариант, - "прожигать" фьюз-биты.
Итак, в нашем случае, ответы на три озвученных ранее вопроса были такими:
Образ хранится в микросхеме памяти. SPI NOR флешка.
Флешка прошивается уже будучи распаянной на плате.
"Прожиг" фьюз битов.
Стенд
Решением задачи при такой постановке вопроса стал разработанный стенд серийной прошивки и первоначального тестирования.
Устройство, под которое был разработан стенд:
![](https://habrastorage.org/getpro/habr/upload_files/5c9/9e2/714/5c99e27146ac6af2019df912d1e3ca69.jpg)
SoM-модуль, на борту стандартный для SoM-модулей набор: Процессор, память RAM, память flash, физика ethernet, физика wi-fi. По всем четырем граням расположены контактные площадки в форме полу-отверстий с шагом 1.5мм.
А вот и собственно сам стенд:
![](https://habrastorage.org/getpro/habr/upload_files/b6a/10c/a3b/b6a10ca3bdbee34db19c7cb9358dd8be.jpg)
![](https://habrastorage.org/getpro/habr/upload_files/ea4/4ec/ca4/ea44ecca4e16a6260c0c0b0a16d289d7.jpg)
![](https://habrastorage.org/getpro/habr/upload_files/ca3/672/ad0/ca3672ad0bbf9ed42129e57f625389b1.jpg)
С аппаратной точки зрения, все довольно тривиально: по центру набор пружинных контактов под тестируемый SoM, который прижимается сверху вертикальным зажимом. Из подведенной к модулю периферии: UART, JTAG, Ethernet. Ну и питание.
Алгоритм работы стенда
В ручную, процедура прошивки и прожига фьюз-битов выглядит следующим образом:
Тестируемое устройство устанавливается в стенд
подключается usb-uart переходник, программатор JLink, Ethernet-кабель, подается питание.
Запускается openocd сервер (openocd.exe);
Производится подключение к openocd серверу по telnet (openocd клиент);
Открывается COM-порт, назначенный USB-UART переходнику;
Сброс процессора (через консоль openocd-клиента):
reset init; arm core_state arm; halt;
Грузим образ u-boot (через консоль openocd-клиента):
load_image u-boot.imx 0x877ff400
Грузим образ ядра linux (через консоль openocd-клиента):
load_image openwrt-imx6ull-cortexa7-video-squashfs.mtd-factory.bin 0x82000000
Запускаем работу процессора (через консоль openocd-клиента):
resume 0x87800000
В это время должен стартануть u-boot и в консоли com-порта появятся логи загрузки. Нажимаем enter, чтобы остаться в консоли u-boot.
Определите флешку (консоль COM-порта):
=> sf probe
И прошейте флешку (консоль COM-порта):
=> sf update 0x82000000 0x1000 0x8000
Прожгите фьюз-биты (консоль COM-порта):
=> fuse prog -y 0 5 0x0a000030
=> fuse prog -y 0 6 0x00000010
Ребутните процессор (консоль COM-порта):
=> reset
После этого процессор должен ребутнуться и загрузить ядро линукс с флешки без помощи программатора.
Автоматизируй это
Естественно, шаги с 3-его по 13-й можно автоматизировать. По-питонячьи это выглядит так:
# 1. Init OpenOCD server
device_state = Queue()
p_openocd = Process(target=run_openocd_server, args=(device_state,))
p_openocd.start()
check_state(device_state, STATE_JTAG_DEVICE_DETECTED)
# 2. Init. Connect to OpenOCD server via Telnet
telnet_socket_write_queue = Queue()
p_telnet = Process(target=run_telnet_client, args=(telnet_socket_write_queue,))
p_telnet.start()
check_state(device_state, STATE_TELNET_CLIENT_CONNECTED)
# 3. Init. Open COM port.
device_state_com = Queue()
com_port_write_queue = Queue()
p_com_port = Process(target=run_com_port_handler, args=(device_state_com,com_port_write_queue,CFG_COM_PORT,))
p_com_port.start()
# 4. Reset CPU
telnet_socket_write_queue.put((TELNET_CMD_RESTART_CPU + "\r\n").encode())
check_state(device_state, STATE_CPU_RESET_COMPLETED)
# 5. Load u-boot image
telnet_socket_write_queue.put((TELNET_CMD_LOAD_U_BOOT_IMAGE + "\r\n").encode())
check_state(device_state, STATE_IMAGE_LOADED, 30)
# 6. Load kernel image
telnet_socket_write_queue.put((TELNET_CMD_LOAD_KERNEL_IMAGE + "\r\n").encode())
check_state(device_state, STATE_IMAGE_LOADED, 600)
# 7. Start U-boot execution
telnet_socket_write_queue.put((TELNET_CMD_START_U_BOOT + "\r\n").encode())
check_state(device_state_com, STATE_U_BOOT_CONSOLE_ACCESSED)
# 8. Check flash IC available
com_port_write_queue.put(COM_CMD_CHECK_FLASH + "\r")
check_state(device_state_com, STATE_FLASH_IC_DETECTED)
# 9. Flash firmware to IC
com_port_write_queue.put(COM_CMD_WRITE_FIRMWARE_TO_FLASH + "\r")
check_state(device_state_com, STATE_FIRMWARE_FLASHED_TO_IC, 300)
# 10. Burn fuse bit (ECSPI3 programming 0x450 = 0x0a000030)
com_port_write_queue.put(COM_CMD_BURN_FUSE_BITS_1 + "\r")
check_state(device_state_com, STATE_FUSE_BITS_BURNED)
# 11. Burn fuse bit (BT_FUSE_SEL programming 0x460 = 0x00000010)
com_port_write_queue.put(COM_CMD_BURN_FUSE_BITS_2 + "\r")
check_state(device_state_com, STATE_FUSE_BITS_BURNED)
# 12. Reset device
com_port_write_queue.put(COM_CMD_RESET_DEVICE + "\r")
Исходники можно найти тут. Скрипт может быть относительно легко адаптирован под другие семейства процессоров с архитектурой ARM (семейства IMX компании NXP, по крайней мере).
TO-DO
В планах - замена шага №6 (загрузка factory образа по JTAG занимает очень много времени) на загрузку с TFTP-сервера с помощью u-boot утилиты tftp.