Захотел собрать CarPC в Opel Astra H. Родной монохромный экран убрать нельзя, так как он является шлюзом между разными CAN-шинами и на него выводится много полезной информации. Оставлять второй экран в авто тоже не хочу. Потому буду парсить данные с шины экрана и выводить через композитный видеовыход, используя FPGA.
Предыстория
Более 10 лет назад, когда было много свободного времени и чесались руки, я, вдохновившись чужими CarPC проектами, тоже собрал себе компьютер в машину (Москвич 214145). В то время подрабатывал системным администратором и мне стало очень удобно не таскать с собой каждый день ноутбук.
Размещение экрана было очень неудобным, но тогда не было такой доступной 3D печати и выкручивались как могли. На фото CarPC установлен в торпедо от Opel Vectra A.
Настоящее время
Шло время, все менялось и теперь у меня есть автомобиль Opel Astra H, больше знаний и опыта, но «дурная голова рукам покоя не дает» и я решил снова собрать CarPC.
Цель всего проекта не CarPC, как результат, а самообразование. На рынке много готовых решений, которые будут дешевле. Я лишь делюсь описанием небольшого этапа.
Консоль
Следующим образом выглядит торпедо в автомобиле Opel Astra H
Штатный монохромный графический экран имеет удобное местоположение на торпедо, можно его убрать, распечатать переходную рамку под новую матрицу, но есть большое НО: штатный экран (у меня это трехстрочный монохромный графический экран, именуемый как GID) является шлюзом между разными CAN-шинами. Если его отключить, то не будет работать климатическая система.
Умельцы переносят этот экран в потолок. Печатают рамку на центральную часть магнитолы и переносят туда, но все это требует доработки штатной проводки, либо создание удлинителей. Мне кажется, что два экрана в авто – это перебор.
Можно спрятать GID за торпедо, но на него выводится много полезной информации и не хочется ее терять.
В 2020 году купил подходящую матрицу экрана и плату конвертера на контроллере RTD2662. Напечатал рамку, примерил и на этом мои силы кончились, так как ушли в домашний ремонт.
Прошло 5 лет, я снова вернулся к этому увлекательному проекту. Но пора заканчивать эту небольшую лирику и переходить к сути
Ломаем GID
Я решил и GID спрятать и информацию с него получать.
Можно ловить CAN-пакеты. Благодаря замечательному сообществу, именуемому как «Astra H CAN хакеры», декодировано и разобрано на биты огромное количество пакетов и параметров, которые в них содержатся.
Но я решил пойти другим путем: буду ловить данные, которые идут непосредственно на матрицу экрана. Таким образом плата от GID останется на родном месте.
Чтобы случайно не сломать мой экран, приобрел такой-же на разборке. Чтобы он включился на столе – необходима магнитола и соединить их по CAN шине. В таком случае экран включается при включении магнитолы.
Матрица экрана с кодовым названием 80509CN выглядит следующим образом:
Что примечательно – у нее два шлейфа. Ранее мне такие не попадались. По всевозможным номерам, которые есть на матрице – не ищется никакой документации. Значит буду проводить обратную разработку.
Матрица подключается к основной плате GID-а.
Осмотрев плату, стало понятно, что слоев в ней больше, чем два, так как некоторые переходные отверстия от сигнальных дорожек с одной стороны платы, никуда не подключены на обратной стороне.
Установлен микроконтроллер NEC v850eca2. Пролистав документацию, не увидел специализированного контроллера для экрана. Скорее всего экран сидит или на внешней шине данных, или на GPIO.
Не стал реверсить схему подключения экрана к микроконтроллеру, а решил пробежать по выводам. Внешне похоже, что оба разъема экрана имеют одинаковую распиновку. Прозвонил выводы двух разъемов на взаимное соединение, отметил КРАСНЫМ – линии, которые попарно соединены, а ЖЕЛТЫМ – которые у каждого разъема индивидуальны.
И действительно, большее число выводов подключено параллельно.
Случайно наткнулся в интернете на информацию, что если перевернуть экран на 180 градусов и подключить, то он тоже будет работать (внутри корпуса есть выступы, препятствующие этому, но один человек отколол часть матрицы и у него перестала работать нижняя половина, на которой наибольшее количество важной информации. Он удалил ограничители и перевернул экран)
Получается, что внутри экрана ДВА одинаковых контроллера. Что сильно облегчает дальнейшую работу.
Далее я взял осциллограф и начал смотреть, как себя ведут разные линии.
На фотографии ниже сделал отметки: если напряжение стабильное, то указал уровень. Сигнал положительной полярности (преобладает низкий уровень), то отметил как «p», сигнал отрицательной полярности (преобладает высокий уровень), то отметил как «n», если идет постоянный меандр, то указал частоту и заполнение. Цифровые сигналы имеют лог. Уровень 5в.
Из этой картины явно выделяется шина данных на 8 бит и сигналы выбора верхнего/нижнего контроллера (единственный сигнальный вывод на разъеме, который не объединен).
Подключил логический анализатор DSLogic, начал смотреть обмен на шине.
Сняв лог с выборкой 10ns в течение 1с, начал его просматривать и обнаружил интересные данные
Битовые данные на 8-битной шине повторяют надпись, которая отображается на экране.
Также увидел сигналы выбора верхнего/нижнего контроллера (на картинке - зеленым)
Сигнал отрицательной полярности, скорее всего – nWE, по его спаду (заднему фронту) контроллер защелкивает данные (на картинке - красным)
Далее по диаграмме нашел место, где начинается выборка нижнего контроллера.
После перехода должны идти команды. И действительно, последняя неизвестная линия – является сигналом выбора данные/команда. На картинке – оранжевым.
Подсчитал разрешение матрицы
Получилось, что GID у Astra H имеет разрешение 218х138 точек.
Анализируем полученные данные
Небольшая часть работы с паяльником уже проделана.
Предполагаю, что матрица экрана изготовлена под заказ GM, но стоит ли внутри известный контроллер или какой-то свой? Что ж, в этот момент я надеялся, что если контроллер и проприетарный, то писался с взглядом на существующие.
За полный цикл выдачи данных на одну верхнюю половину экрана, происходят 8 посылок следующего содержания: 3 байта команд и 208 байт данных.
Рассмотрим эти 3 байта команд. На картинке начало 4х посылок
В них последовательно инкрементируется единичка в первом командном слове. Таким образом становятся понятны MSB и LSB шины данных.
Полученная распиновка экрана на картинке:
Но вернемся к нашим четырем посылкам. Как же удобны цифровые анализаторы с программными декодерами.
Но посмотрим не только их, а все команды
8 команд перед посылками для верхней половины экрана:
[0xB0, 0x10, 0x05]
[0xB1, 0x10, 0x05]
[0xB2, 0x10, 0x05]
[0xB3, 0x10, 0x05]
[0xB4, 0x10, 0x05]
[0xB5, 0x10, 0x05]
[0xB6, 0x10, 0x05]
[0xB7, 0x10, 0x05]
8 команд перед посылками для нижней половины экрана:
[0xB0, 0x12, 0x05]
[0xB1, 0x12, 0x05]
[0xB2, 0x12, 0x05]
[0xB3, 0x12, 0x05]
[0xB4, 0x12, 0x05]
[0xB5, 0x12, 0x05]
[0xB6, 0x12, 0x05]
[0xB7, 0x12, 0x05]
Ширина экрана 218, но данных приходит только на 208. Но и на экране слева и справа отступы по 5 пикселей. Скорее всего 3-й байт данных – номер столбца, а младшие 4 бита в первом байте команды – номер «строки». Почему в кавычках – данные выдаются сразу на 8 строк. Получается, что это больше указатель на сектор строк.
После вышеуказанных 16 больших команд с данными приходит «дозаполнение» пустых ячеек.
Тут видно, что половины экрана «независимы». Приходят команды для каждой половинки, а затем данные идут сразу на две половинки.
Предположу, что внутри основного микроконтроллера NEC v850eca2 буфер видеокадра рассчитан на 208x128 точек, так как некоторые строки и столбцы прячутся за рамку экрана.
После полного заполнения «видимой части экрана», «дозаполнение» приходит на все оставшиеся пиксели (в том числе обрезки строк, шириной по 5 пикселей).
Экран обновляется раз в секунду.
Помимо буфера обновления экрана – иногда по шине приходят команды конфигурации экрана.
Из всех вышеперечисленных данных я нашел наиболее подходящий, по командам и описанию, контроллер - ST7565, только в моем случае – разрешение экрана побольше.
Но не все команды удалось найти в описании. Вот пример конфигурации экрана:
В машине, в настройках экрана можно включать инверсию. Теперь я понял, что передача буфера данных на экран не меняется – меняется только конфигурация для включения инверсии.
Пишем программный декодер
В целом, собранных данных достаточно, чтобы написать примитивный декодер. Его я буду писать на C# и выводить графику.
Для декодирования буду использовать собранный логическим анализатором дамп.
Выгружу его в формат csv и буду читать из файла.
Алгоритм работы программы, следующий:
С делал PictureBox размером 872x512 пикселей – в 4 раза больше, чем реальное разрешение экрана, буду выводить точки размером 4х4 (чтобы было не мелко). Сам видеобуфер размером 218x16 байт.
В обработчике PictureBox_Paint() буду отрисовывать данные из видеобуфера.
В основном цикле программы я пробегаю по всем строкам входного файла, разбираю значения, храню прошлое состояние nWE. Ищу спад (задний фронт), т.е. если старое значение 1, а новое 0, то выполняю действие.
Завел две глобальные переменные на номер строки и столбца.
Дополнительно у меня есть флаг валидности данных.
Фильтрую первую пришедшую команду по маске 0xF8. Если команда по маске 0xB0, беру из нее номер строки по маске 0x07. Если при этом выбрана верхняя часть экрана, то в номер строки пишу этот номер, если выбрана нижняя, то этот номер + 8. Следующие две команды – вспомогательные. Вторую не обрабатываю, из третьей беру номер столбца. Если эти три команды прошли, то поднимаю флаг валидности данных, что следующие данные можно писать в буфер. Каждая запись инкрементирует номер столбца.
Если в первую пришедшую команду по маске 0xF8 пришла команда отличная от 0xB0, то снимаю флаг валидности данных.
На картинке результат работы программного декодера.
Пишем аппаратный декодер
Для аппаратного декодера я использовал ПЛИС altera cyclone iv ep4ce10. У меня как раз лежит одна с выгоревшим jtag портом, но без проблем считывает конфигурацию с SPI флеш-памяти. У нее на борту есть аппаратные блоки двухпортовой SRAM-памяти. Это очень полезно, так как чтение и запись будут производиться разными модулями.
Так как уровни логических сигналов GID 5в, а у моей ПЛИС 3.3в, были использованы микросхемы для согласования уровней. У них есть сигнал OE. Его подключил к выходу ПЛИС, где генерирую постоянную лог «1». ПЛИС запускается не сразу, а сначала считывает конфигурацию. Чтобы в этот момент не было конфликтов (согласователи с автоматическим определением направления) – решил сделать так.
Для аппаратного декодера перенес с небольшими изменениями мою программную реализацию. Никаких оптимизаций, просто чтобы работало.
module decoder(
input clk,
input [7:0] data,
input nWE,
input DC,
input CS_up,
input CS_down,
output [12:0] mem_address,
output [7:0] mem_data,
output mem_we,
output mem_clk
);
reg [1:0] cmd_pos = 2'd0;
reg decoder_mem_we = 1'b0;
reg [7:0] col_adr = 8'h00;
reg [3:0] row_adr = 4'h0;
reg reg_mem_clk = 0;
reg mem_clk_cnt = 0;
always @(posedge nWE) begin
if(DC == 1'b0) begin
if(cmd_pos == 2'd0) begin
if((data & 8'hF8) == 8'hB0) begin
if(CS_up) begin
row_adr <= data & 8'h0F;
col_adr <= 0;
end else if(CS_down) begin
row_adr <= (data & 8'h0F) + 8'd8;
col_adr <= 0;
end
cmd_pos <= 1;
end
decoder_mem_we <= 1'b0;
end else if(cmd_pos == 2'd1) begin
col_adr <= col_adr | (data & 8'h01)<<4;
cmd_pos <= 2'd2;
end else if(cmd_pos == 2'd2) begin
col_adr <= col_adr | (data & 8'h0F);
cmd_pos <= 2'd0;
decoder_mem_we <= 1'b1;
end
end
if(decoder_mem_we & DC) begin
col_adr <= col_adr + 1;
end
end
assign mem_we = decoder_mem_we & DC & ~nWE;
assign mem_address = {row_adr[3:0], col_adr[7:0]};
assign mem_data = data;
always @(posedge clk)begin
if(~nWE) begin
if(~mem_clk_cnt) begin
if(~reg_mem_clk)begin
reg_mem_clk <= 1'b1;
end else begin
reg_mem_clk <= 1'b0;
mem_clk_cnt <= 1'b1;
end
end
end else begin
reg_mem_clk <= 1'b0;
mem_clk_cnt <= 1'b0;
end
end
assign mem_clk = reg_mem_clk;
endmodule
Код синтезировался и даже работает
Для отладки выводил данные, которые пишутся в SRAM на внешние выводы, но потом понял, что сразу в железе отладить не получится и придется писать тестбенч.
Взял добрый уютный iverilog. Очень мало времени работаю с ПЛИС, не знаю многих подходов, потому вместо чтения csv файла средствами iverilog – в моем программном декодере написал конвертер входных данных в строки для тестбенча (да закидают меня тапками, сейчас я уже знаю как правильно, но на тот момент это было быстрое рабочее решение)
Получил строки вида:
И с головой ушел в моделирование
В результате – модуль декодера стучится в SRAM, генерирует необходимые сигналы: адрес, данные, разрешение записи и clk для защелкивания.
Пишем аппаратный вывод данных
Вывод данных должен быть универсальным. В том числе, чтобы в машину (кто еще не потерял нить рассуждений и дочитал до этого момента, я же делаю все это для машины) можно было поставить китайскую магнитолу, а ее экран перенести наверх (если я не доберусь до постройки CarPC). Обычно у магнитол из доступных разъемов – только CVBS, а у используемого мной контроллера экрана RTD2662 доступно до 4х аналоговых видеовходов, то его и будем использовать.
Для аналогового вывода использовал 8-битный DAC R2R (но на каждый выход использовал 2 порта ввода-вывода, чтобы повысить ток. В общей сложности использовал 16 портов), а сигнал синхронизации - суммировал к текущему уровню.
Одна статья с хабра очень помогла быстро запустить CVBS и написать его заново под свои нужды.
Начал разбираться и заметил особенность. Что в моей реализации, что в реализации из статьи: если попытаться передать чередующиеся строки белая-черная-белая, то экран вместо строк начинает мерцать. Если 2 строки белые – 2 черные – 2 белые, то все ОК, но разрешение падает в 2 раза (мне не критично, т.к. мне нужно вообще 128 строк).
Под рукой всегда держал шпаргалку:
Затем сделал некоторые свои тесты с нужным разрешением экрана
Таким получился мой модуль вывода Monochrome Composite Video
module PAL(
input clk,
output [15:0]mem_address,
input mem_data,
output mem_clk,
output [7:0]video_out
);
wire sync;
reg frame = 0;
wire [7:0]video_in;
assign video_in = (mem_data) ? BRIGHT : 8'd0; //test
wire [7:0]video;
assign video = (frame) ? video_in : 8'd0;
assign video_out = (sync) ? (video + 8'd25) : 8'd0;
localparam BRIGHT = 8'h80;
localparam BROAD_SYNC_SECTION1 = 5'd0; //5
localparam SHORT_SYNC_SECTION1 = 5'd1; //5
localparam EMPTY_LINE1 = 5'd3; //18
localparam FULL_LINE1 = 5'd4; //287
localparam SHORT_SYNC_SECTION2 = 5'd5; //5
localparam BROAD_SYNC_SECTION2 = 5'd6; //5
localparam SHORT_SYNC_SECTION3 = 5'd7; //5
localparam EMPTY_SYNC_SECTION = 5'd8; //1
localparam EMPTY_LINE2 = 5'd10; //17
localparam FULL_LINE2 = 5'd11; //287
localparam SHORT_LINE = 5'd12; //1
localparam SHORT_SYNC_SECTION4 = 5'd13; //5
localparam X_END_HALF_LINE = 12'd1600;
localparam X_END_FULL_LINE = 12'd3200;
localparam IMAGE_START_CLK = 12'd573;//12'd520;
localparam IMAGE_STOP_CLK = 12'd2971;//12'd2923; 2644-900 = 8clk/1px
reg [12:0]x_end_counter = 12'd0;
reg [12:0]y_end_counter = 12'd0;
reg [12:0]sync_start_counter = 12'd0;
reg [12:0]x_clk_count = 12'd0;
reg [12:0]y_clk_count = 12'd0;
reg [5:0] current_state = 6'h00;
reg [5:0] next_state = 6'h00;
reg [8:0] count_states_max = 9'h000;
reg [12:0]image_clk_count = 12'd0;
reg [9:0]x_pos = 10'h000;
reg [3:0]x_tmp_pos = 4'd0;
//assign x_pos[9:0] = image_clk_count[12:3];
wire [12:0]y_tmp_clk_count;
assign y_tmp_clk_count = y_clk_count - 16;
wire [9:0]y_pos;
assign y_pos[9:0] = y_tmp_clk_count[10:1];
always @(posedge clk) begin
if(x_clk_count >= x_end_counter - 1) begin
frame <= 0;
x_clk_count <= 12'd0;
image_clk_count <= 12'd0;
x_pos <= 10'd0;
if(y_clk_count >= y_end_counter - 1) begin
current_state <= next_state;
y_clk_count <= 12'd0;
end else begin
y_clk_count <= y_clk_count + 1;
end
end else begin
x_clk_count <= x_clk_count + 1;
if(x_clk_count >= IMAGE_START_CLK && x_clk_count < IMAGE_STOP_CLK && (current_state == FULL_LINE1 || current_state == FULL_LINE2))begin
if(y_clk_count >= 16 && y_clk_count < 272) //16-271 ; 256/64 4line/1px
begin
frame <= 1;
end else begin
frame <= 0;
end
//if(image_clk_count )
if(x_tmp_pos == 4'd10)begin
x_tmp_pos <= 0;
x_pos <= x_pos + 1;
end else begin
x_tmp_pos <= x_tmp_pos + 1;
end
image_clk_count <= image_clk_count + 1;
end else begin
frame <= 0;
end
end
end
assign sync = (x_clk_count >= sync_start_counter) ? 1'b1 : 1'b0;
always @(*) begin
case (current_state)
BROAD_SYNC_SECTION1: begin //0 - 2.5
next_state = SHORT_SYNC_SECTION1;
x_end_counter = X_END_HALF_LINE;
y_end_counter = 12'd5;
sync_start_counter = 12'd1365;
end
SHORT_SYNC_SECTION1: begin //2.5 - 5
next_state = EMPTY_LINE1;
x_end_counter = X_END_HALF_LINE;
y_end_counter = 12'd5;
sync_start_counter = 12'd120;
end
EMPTY_LINE1: begin //7-23
next_state = FULL_LINE1;
x_end_counter = X_END_FULL_LINE;
y_end_counter = 12'd18;
sync_start_counter = 12'd235;
end
FULL_LINE1: begin //24-310
next_state = SHORT_SYNC_SECTION2;
x_end_counter = X_END_FULL_LINE;
y_end_counter = 12'd287;
sync_start_counter = 12'd235;
end
SHORT_SYNC_SECTION2: begin //311-312.5
next_state = BROAD_SYNC_SECTION2;
x_end_counter = X_END_HALF_LINE;
y_end_counter = 12'd5;
sync_start_counter = 12'd120;
end
BROAD_SYNC_SECTION2: begin //312.5-315
next_state = SHORT_SYNC_SECTION3;
x_end_counter = X_END_HALF_LINE;
y_end_counter = 12'd5;
sync_start_counter = 12'd1365;
end
SHORT_SYNC_SECTION3: begin //316-317.5
next_state = EMPTY_SYNC_SECTION;
x_end_counter = X_END_HALF_LINE;
y_end_counter = 12'd5;
sync_start_counter = 12'd120;
end
EMPTY_SYNC_SECTION: begin //317.5
next_state = EMPTY_LINE2;
x_end_counter = X_END_HALF_LINE;
y_end_counter = 12'd1;
sync_start_counter = 12'd0;
end
EMPTY_LINE2: begin
next_state = FULL_LINE2;
x_end_counter = X_END_FULL_LINE;
y_end_counter = 12'd17;
sync_start_counter = 12'd235;
end
FULL_LINE2: begin
next_state = SHORT_LINE;
x_end_counter = X_END_FULL_LINE;
y_end_counter = 12'd287;
sync_start_counter = 12'd235;
end
SHORT_LINE: begin
next_state = SHORT_SYNC_SECTION4;
x_end_counter = X_END_HALF_LINE;
y_end_counter = 12'd1;
sync_start_counter = 12'd235;
end
SHORT_SYNC_SECTION4: begin
next_state = BROAD_SYNC_SECTION1;
x_end_counter = X_END_HALF_LINE;
y_end_counter = 12'd5;
sync_start_counter = 12'd120;
end
endcase
end
assign mem_address[15:0] = {y_pos[6:3], x_pos[7:0], y_pos[2:0]};
assign mem_clk = clk & frame;
endmodule
Много лишнего, не оптимально, так что прошу строго не судить.
Модуль верхнего уровня и память
Память двухпортовая. Причем на вход она 8-битная, а на выход – 1-битная.
Отлаживать и моделировать пришлось долго, так как было много глупых ошибок, вроде такой:
Так как моделирую в iverilog, то накидал простую модель
module SRAM(
input [12:0] address_a,
input [7:0] data_a,
input clock_a,
input wren_a,
output [7:0] q_a,
input [15:0] address_b,
input [7:0] data_b,
input clock_b,
input wren_b,
output q_b
);
reg [7:0]mem[8191:0];
reg [7:0]r_q_a = 8'h00;
assign q_a = r_q_a;
reg r_q_b = 1'b0;
assign q_b = r_q_b;
always @(posedge clock_a) begin
r_q_a <= mem[address_a];
if(wren_a == 1'b1)begin
mem[address_a] <= data_a;
end
end
always @(posedge clock_b) begin
r_q_b <= mem[address_b[15:3]][address_b[2:0]];
if(wren_b == 1'b1)begin
mem[address_b] <= data_b;
end
end
endmodule
Вот как выглядит мой TOP-модуль
module GID(
input clk,
input [7:0]lcd_data,
input lcd_nWE,
input lcd_DC,
input lcd_CS_up,
input lcd_CS_down,
output [7:0]LED,
output driver_oe,
output [7:0]video_out,
output [7:0]video_out2
);
assign driver_oe = 1'b1;
wire [7:0]video;
assign video_out = video;
assign video_out2 = video;
wire [7:0] test_ledout;
assign test_ledout[7:0] = address_a[7:0];
assign LED = ~test_ledout;
wire [12:0] address_a;
wire [7:0] data_a;
wire wren_a;
wire mem_clk_a;
wire [15:0] address_b;
wire data_b;
wire mem_clk_b;
decoder decoder(
.clk ( clk ),
.data ( lcd_data ),
.nWE ( lcd_nWE ),
.DC ( lcd_DC ),
.CS_up ( lcd_CS_up ),
.CS_down ( lcd_CS_down ),
.mem_address ( address_a ),
.mem_data ( data_a ),
.mem_we ( wren_a ),
.mem_clk ( mem_clk_a )
);
SRAM SRAM(
.address_a ( address_a ),
.data_a ( data_a ),
.clock_a ( mem_clk_a ),
.wren_a ( wren_a ),
.q_a ( ),
.address_b ( address_b ),
.data_b ( ),
.clock_b ( mem_clk_b ),
.wren_b ( 1'b0 ),
.q_b ( data_b )
);
PAL PAL(
.clk ( clk ),
.mem_address ( address_b ),
.mem_data ( data_b ),
.mem_clk ( mem_clk_b ),
.video_out ( video )
);
endmodule
Схема в блоках выглядит даже не плохо, если не залезать внутрь
Результат
Ну и вот что мы имеем на выходе:
Понимаю, что все не очень оптимально, сделано на коленке за 3-4 вечера, но я очень доволен результатом и много чего полезного узнал.
Спасибо за внимание.
[Предыстория]
В недалеком прошлом я стал обладателем удобной (по моему мнению) клавиатуры Logitech K800. Данная клавиатура является мембранной, но механизм переключения кнопок – «ножницы», как на ноутбуках, правда с бОльшим ходом. Дополнительно она имеет приятную белую подсветку.
С клавиатурой есть три проблемы:
Настал решающий момент заменить клавиатуру. Прогулявшись по интернет-магазинам, я понял, что ничего из предложенного среди мембранок не нравится. Рекомендации ютуба начали выдавать обзоры различных механических клавиатур, кастомы и прочее. Мне никогда не нравился огромный ход механики, потому стандартные варианты не подошли, но как-то раз в рекомендациях Aliexpress показались механические переключатели kailh low profile. И я понял, что это – оно. (сейчас уже доступны для покупки низкопрофильные механики, например, Logitech). Решено - сделаю свой хардкорный кастом.
Процесс сборки клавиатуры с нуля занял у меня без малого, два года в неспешном ритме, но обо всем по порядку.
[Выбор переключателей]
Все механические переключатели имеют свои особенности: Сила нажатия, момент срабатывания, обратная связь. Когда я читал информацию и слушал записи «тейпинга», то не смог в слепую определиться с типом переключателей, потому заказал «набор - пробник» из 13 различных переключателей. Какие-то со щелчком, какие-то без. Напечатал на 3D принтере матрицу для их фиксации.
Распределил их по силе нажатия и начал выбирать. Здесь я сделал небольшую ошибку: необходимо все переключатели смазать, чтобы они работали правильно. Эта ошибка не повлияла на мой выбор и вариант с коричневыми переключателями оказался наиболее удобным.
Следующим этапом было решение вопроса с кейкапами. Крепеж у данных переключателей отличается от стандартных, высоких переключателей. К этому моменту начали появляться статьи с кастомами на данных переключателях, но все использовали одинаковый (1u) размер кейкапа для всех клавиш, что мне не подходило. Так как готовых наборов кейкапов не было, пришлось вновь изобретать велосипед. Нашел наиболее универсальную форму кейкапа среди 3D моделей, которые представлены в открытом доступе. Смоделировал крепление и попробовал распечатать на 3D принтере. Высота слоя 0.1, сопло 0.3. Печатал белым ABS и светопрозрачным PETg пластиками. Результат на фото:
Результат меня устроил, как по форме, так и по качеству кейкапа, значит с вариантом изготовления было решено – печатать. Хотелось организовать подсветку - поэтому все спроектированные кейкапы в дальнейшем решил печатать светопрозрачным PETg пластиком. Далее покраска, лазерная гравировка и лак, но обо всем по порядку.
[Выбор форм-фактора и раскладки]
Следующим этапом был выбор форм-фактора. Как писал выше, ISO меня не устроил, потому – ANSI. Никаких вычурных форм и т.п., обычная полноценная клавиатура с NumPad- блоком, но захотелось мультимедийных клавиш – их 4шт. над блоком NumPad. Не нашел в свободной продаже стабилизаторов на длинные кейкапы, стал делать без них. По этой причине пробела – два (таких решений встречал много).
[Схема матричного опроса клавиатуры]
Как организовывать опрос клавиш. В моем случае получилось 109 переключателей. Вешать каждый на отдельную ногу целевого микроконтроллера конечно можно, но это дорогой путь. Организовал матричный опрос клавиатуры (6 строк, 20 столбцов) с использованием диодов на каждой клавише (чтобы избежать фантомных нажатий). Про подобный способ опроса клавиатуры существует множество статей, попробую вкратце:
Столбцы – входы микроконтроллера с внутренней подтяжкой к лог. «1».
Строки – выходы микроконтроллера.
В неактивном режиме: на всех выходах (строках) установлена лог. «1», на всех входах (столбцах) активна встроенная подтяжка, она перетягивает вход в лог. «1».
Во время опроса матрицы на выходы (строки) последовательно подаются лог. «0» и на каждом таком шаге происходит опрос всех входов (столбцов). Если на выбранной строке нажата клавиша, то логический уровень соответствующего подтянутого входа столбца перейдет в лог. «0». Так как каждый шаг задается пользователем, то нажатая клавиша определяется однозначно. На фото представлен опрос по шагам:
Теоретически можно избежать инверсии, если изменить направление диода и на выходы (строки) подавать лог. «1».
В мембранных клавиатурах не используются диоды и алгоритм опроса немного отличается. Описывать все не буду, информации в интернете достаточно. (чаще используются входы с открытым коллектором в совокупности с оптимальной матрицей и дополнительными проверками).
[STM32]
Сигнальным сердцем клавиатуры выступил микроконтроллер STM32F103C8T6. У данного камня на борту есть необходимые интерфейсы: USB, таймеры с ШИМ (в дальнейшем пригодятся для подсветки), DMA, достаточное количество выводов для матричного опроса кнопок.
На фото выше представлена использованная распиновка микросхемы в CubeMX.
Изначально работу по отладке USB (до заказа печатной платы) я производил на плате BluePill. Расположение USB (DP/DN) остается на тех же выводах.
Далее я бы рекомендовал прочитать документ «HID Usage Tables for Universal Serial Bus (USB)», чтобы написанное ниже было понятней.
В CubeMX, в разделе «Middleware» настроил USB_DEVICE в режиме HID, интервал опроса сделал минимальным (1мс).
Далее пришлось разобраться с дебрями USB. Самым сложным оказался «Report Descriptor». У меня получилось 2 устройства (2 точки монтирования, если я правильно понимаю). Первое устройство – клавиатура с индикацией состояний (Num/Scroll/Caps locks), второе – мультимедийное устройство с клавишами управления (В данный момент использую 4: Play/Pause, Vol-, Vol+, Mute).
[Дескрипторы]
Дескриптор клавиатуры можно сделать под свои задачи, но чтобы клавиатура работала в BIOS, необходимо соответствовать стандартному дескриптору из стандарта. Вот что получилось:
__ALIGN_BEGIN static uint8_t HID_KEYBOARD_ReportDesc[HID_KEYBOARD_REPORT_DESC_SIZE] __ALIGN_END =
{
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x06, // USAGE ID (Keyboard)
0xA1, 0x01, // COLLECTION (Application)
0x85, 0x01, // Report ID (1)
0x05, 0x07, // USAGE_PAGE (Keyboard)
0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl)
0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x75, 0x01, // REPORT_SIZE (1)
0x95, 0x08, // REPORT_COUNT (8)
0x81, 0x02, // INPUT (Data,Var,Abs) ;Keys byte
0x05, 0x07, // USAGE_PAGE (Keyboard)
0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated))
0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x65, // LOGICAL_MAXIMUM (101)
0x75, 0x08, // REPORT_SIZE (8)
0x95, 0x06, // REPORT_COUNT (6)
0x81, 0x00, // INPUT (Data Array)
0x05, 0x08, // USAGE_PAGE (LEDs)
0x09, 0x01, // Usage (Num Lock)
0x09, 0x02, // Usage (Caps Lock)
0x09, 0x03, // Usage (Scroll Lock)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x03, // Report Count (3)
0x91, 0x02, // OUTPUT (Constant Array) ;LED report padding
0x75, 0x05, // REPORT_SIZE (5)
0x95, 0x01, // REPORT_COUNT (1)
0x91, 0x01, // OUTPUT (Constant Array) ;LED report padding
0xc0, // END_COLLECTION
0x05, 0x0C, // Usage Page (Consumer)
0x09, 0x01, // Usage (Consumer Control)
0xA1, 0x01, // Collection (Application)
0x85, 0x02, // Report ID (2)
0x05, 0x0C, // Usage Page (Consumer)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x09, 0xB5, // Usage (Scan Next Track)
0x09, 0xB6, // Usage (Scan Previous Track)
0x09, 0xB7, // Usage (Stop)
0x09, 0xB8, // Usage (Eject)
0x09, 0xCD, // Usage (Play/Pause)
0x09, 0xE2, // Usage (Mute)
0x09, 0xE9, // Usage (Volume Increment)
0x09, 0xEA, // Usage (Volume Decrement)
0x75, 0x01, // Report Size (1)
0x95, 0x08, // Report Count (8)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0, // End Collection
};
В конфигурационном дескрипторе (на всякий случай во всех) пришлось изменить количество NumEndpoints и выставить максимальный ток USB (Клавиатура с подсветкой потребляет 850 мА, а в дескрипторе максимум 500 мА. Не уверен, что так можно, но некоторые флагманские клавиатуры потребляют до 2А).
При наличии двух точек монтирования, при отправке репорта (USBD_HID_SendReport), первым байтом идет идентификационный номер точки монтирования. В моем случае для клавиатуры это «1», для мультимедийных кнопок «2».
Репорт клавиатуры выглядит следующим образом:
struct HID_Keys_t {
uint8_t ID;
uint8_t MODIFIER;
uint8_t RESERVED;
uint8_t KEYCODE[6];
};
Т.е. в вышеуказанном случае это 9 байт данных. Если точка монтирования одна, то передается 8 байт данных.
ID – идентификатор, он равен «1»,
MODIFIER – битовая маска системных клавиш (LCTRL, LSHIFT, LALT, LWIN, RCTRL, RSHIFT, RALT, RWIN). Их объявил в дескрипторе (0xE0-0xE7)
RESERVED – зарезервированный байт данных.
KEYCODE[6] – 6 байт данных нажатых клавиш.
Из вышеописанного следует, что по стандарту клавиатура не может передать информацию о более чем 6 одновременно нажатых клавишах (не считая системные). Чтобы передать 12, можно теоретически сделать 2 точки монтирования, если это действительно нужно.
Проблема, которую не смог решить: В результате появления байта ID, ReportDescriptor стал 9 байтным. Последний KEYCODE игнорируется ПК (5 нажатых клавиш одновременно). Мне это не критично, но почему – не знаю. При изменении HID_EPIN_SIZE на 9, USB устройство перестает определяться.
По умолчанию не реализован функционал получения данных от ПК, что потребуется для определения состояний светодиодов на клавиатуре. Пришлось реализовать две функции:
static uint8_t USBD_HID_DataOut(USBD_HandleTypeDef *pdev, uint8_t epnum)
{
HAL_PCD_EP_Receive(pdev->pData, HID_EPOUT_ADDR, rx_buf, HID_EPOUT_SIZE);
return USBD_OK;
}
uint8_t * USBD_HID_GetData(void){
return rx_buf;
}
С USB все получилось (конечно не с первого раза, пришлось просидеть не один вечер), далее необходимо разобраться с подсветкой клавиш.
[Адресные светодиоды]
Для подсветки клавиш решил использовать адресные светодиоды, размером 2х2 мм. В выбранных механических переключателях под подсветку предусмотрено место.
Купленные адресные светодиоды имеют следующий протокол обмена данными:
Все адресные светодиоды соединяются последовательно, каждый «откусывает» себе 24 бита данных (GRB-888), а остальную часть посылки пропускает дальше. Сброс дает команду переключения с текущего цвета на последние полученные данные. Итого 109 светодиодов по 24 бита – это 2616 бит данных. При длительности каждого бита 1.25 мкс + минимальный сброс 50 мкс, получается 3.32мс на одну полную посылку. (теоретически 30 кадров в секунду, если я ничего не напутал)
В STM32 нет аппаратного контроллера для адресных светодиодов, но есть гибкий ШИМ + DMA.
В контроллере TIM2 я настроил таймер так, что при частоте ядра 72МГц, частота ШИМ была 800кГц (разделив на 90), а период ШИМ как раз 1.25мкс.
Задав длительность ШИМ в 58 (из 90), получается длительность лог «1» в 0.806мкс, а задав 29 (из 90), получается длительность лог «1» 0.403мкс. В первом случае получаем цифровую «1», а во втором цифровой «0» протокола адресных светодиодов.
Каждый раз отвлекать ядро на запись данных в регистр ШИМ не рационально, но можно использовать DMA. Сформировав заранее весь кадр данных, можно одной командой запустить передачу данных. Вот протокол и готов. Расплата за удобство – излишнее использование памяти. На каждый бит переданных данных тратится 2 байта ОЗУ. Чтобы посылка была полностью автоматизированной, нужно добавить в начало сброс, а в конце «нулевой» ШИМ.
Для удобства создал структуры:
#define START_COUNT 48UL
#define LED_COUNT 109UL
#define STOP_COUNT 1UL
#define BUF_LEN START_COUNT+LED_COUNT*24+STOP_COUNT
#define LED_HIGH 58
#define LED_LOW 29
struct LEDS_COLOR_t{
uint8_t R;
uint8_t G;
uint8_t B;
};
struct WS2812_LEDS_t{
struct LEDS_COLOR_t num[LED_COUNT];
};
struct WS2812_RAWColor_t{
uint16_t G[8];
uint16_t R[8];
uint16_t B[8];
};
struct WS2812_BufDMA_t{
uint16_t startDelay[START_COUNT];
struct WS2812_RAWColor_t LEDs[LED_COUNT];
uint16_t stopDelay[STOP_COUNT];
};
Структура WS2812_LEDS_t описывает массив цветовой палитры, который доступен для пользователя. Это просто удобно (потратить дополнительно 327 байт уже не кажется проблемой).
Структура WS2812_BufDMA_t описывает расположение данных, которые будут использоваться для DMA. Передав указатель на структуру, фактически передается указатель на массив (здесь это работает, т.к. линковщик дополнительно не выравнивает структуру по 32 битному смещению, в некоторых ситуациях и железе это делать можно, но с костылями).
При инициализации структуры, массивы startDelay и stopDelay заполняются нулевыми значениями.
Процесс использования:
правим кадр (объект структуры WS2812_LEDS_t),
запускаем конвертацию (используя WS2812_LEDS_t генерируем WS2812_BufDMA_t),
запускаем DMA: HAL_TIM_PWM_Start_DMA(&htim2, TIM_CHANNEL_2,(uint32_t*)&WS2812_BufDMA,sizeof(struct WS2812_BufDMA_t)/sizeof(uint16_t));
[Разработка печатной платы]
Прежде чем разрабатывать печатную плату, начал изучать, а какие размеры платы и в какую стоимость выйдет такой заказ. Оказалось, что на популярном (ранее) китайском сайте для заказа печатных плат, максимальный размер без увеличения стоимости за габариты, составил 430мм по одной из сторон. В целом – это оптимальный размер для полноценной клавиатуры с NumPad-ом.
Посчитал, что запаивать переключатели на впервые разрабатываемом девайсе плохая идея – приобрел ответные части для хотсвапа механических переключателей.
Разметил размер 1u клавиши в 18мм, все успешно убралось.
Для согласования сигнального уровня 3.3в от микроконтроллера и 5в светодиодов, на плате поставил согласователь уровней (можно обойтись без него, проинвертировав сигнал через транзистор с подтяжкой к 5В, что тоже будет работать с правильной настройкой ШИМ).
В целом, по плате ничего сложного, пара кнопок на всякий случай, стабилизатор на 3.3в, USB-C. Ввиду достаточно длинных линий, повышается их емкость, пришлось в матричном опросе клавиатуры дополнительно вводить задержку в 20мкс после переключения выходов (чтобы входы, которые переключались на предыдущем шаге, успели «подтянуться» через встроенную подтяжку в лог «1»).
5 печатных плат 430мм х 130мм с пересылкой вышли в 2360р.
Поставил предохранитель на 1А, пришлось ограничить яркость белого света адресных светодиодов до 50%.
[Печать кейкапов]
Так как готовых кейкапов нет, пришлось их моделировать и печатать на 3D принтере самостоятельно. Получились типоразмеры 1u (обычные клавиши), 1.25u (ctrl, alt и т.п.), 1.5u (tab), 1.75u (caps), 2u (num_0), 2.25u (lshift), 2Ru (num_enter) и много нестандартных – под space, rshift, enter и т.п.
Печать происходила соплом 0.3, высота слоя 0.1. Один кейкап 1u печатался 36 минут. Лучше печатать смолой, но такого принтера у меня нет, зато есть 4 FDM
Печать заняла значительное время. Неспешно можно проверять:
Дальше пошел этап ошкуривания всех кейкапов. На полупрозрачном пластике казалось, что результат хороший, но окраска проявила небольшие недостатки.
Для окраски пришлось смоделировать и напечатать фиксаторы под кейкапы
Получилась такая конструкция, которая быстро печатается и позволяет равномерно окрасить кейкапы (если прямые руки и из аэрографа, а не как я, из балончика). Эти же фиксаторы использовались для последующего нанесения матового лака.
Пока ожидал печати фиксаторов, решил смазать все переключатели, т.к. в стоковом состоянии нажатие одной кнопки заставляет «шуршать» ближайшие. Приобрел достаточно дешевую смазку для пластика SW-92SA (возможно, специальные брендовые смазывают лучше, но этой я смазываю пластиковые напечатанные механизмы, и она у меня уже была). Потратил на 109 переключателей 4 часа.
Разбирал тонким пинцетом неспешно. Ни одна пружинка не улетела и не была потеряна.
[Печать корпуса]
Когда начал разрабатывать модель корпуса, понял что не зря приобрел ответные контакты для хотсвапа переключателей. Снимать/устанавливать переключатели пришлось не один раз, особенно при установке «Плейта»
Корпус разрабатывал так, чтобы его можно было напечатать на моем большом принтере (стол 400х250 мм), детали соединялись между собой без склейки.
В итоге весь корпус собирается на винтах m2.5 6мм и 12мм. Верхняя рамка имеет пазы для соединения. Не стал клеить или красить, фиксируется достаточно жестко, а ошкуривать нет желания. Корпус выполнен из черного PETg, смотрится необычно (для тех, кто не сталкивался с 3D печатью).
[Покраска + гравировка]
Покраску кейкапов производил акриловой краской по пластику, которая не требует предварительной грунтовки.
А вот с гравировкой пришлось повозиться. Когда-то давно, в 2016 году я собрал светодиодный лазерный ЧПУ (2W тогда для светодиодного лазера было много). Рама была ужасной, поигравшись с изготовлением печатных плат (выжигание черной краски на текстолите), станок был успешно разобран до лучших времен. Не так давно от одного из проектов осталась рама (CoreXY механика, обрезанная верхушка от проекта 3D-принтера FriBot) с рабочей областью 400х400 мм. Заказал для этой рамы еще одну рельсу на ось X. Перепроектировал крепления, загрузил прошивку GRBL и можно пользоваться.
Напечатанных кейкапов, с учетом брака, было с запасом. Их стал использовать для экспериментов и калибровки лазера.
Для разметки области включал лазер на 0.5% мощности и на скорости 5000мм/мин размечал границу гравируемой области в режиме вектора. Для гравировки было два прохода со 100% мощностью на скорости 3000мм/мин в режиме изображения.
После гравировки вся выгоревшая краска тчательно убиралась ватной палочкой.
После чистки гравировка выглядела достаточно резкой, с небольшими дефектами. После нанесения матового лака, символы приобрели более приятный вид.
[Результат]
Теперь можно поделиться результатом:
В выключенном состоянии (еще старый неудачный корпус):
Во включенном состоянии:
Индикация активных состояний Num/Caps/Scroll locks отображается на подсветке самих клавиш желтым цветом.
Никаких дополнительных эффектов не делал, пользуюсь данной клавиатурой уже 2 месяца. Был один глюк, но он решился увеличением таймаута в матричном опросе клавиатуры, о чем я писал выше.
[Алгоритм работы]
В последней версии прошивки переписал алгоритм работы, чтобы выложить исходники. В микроконтроллере запущены два таймера. Один с периодом 1мс, второй с периодом 10мс. Оба таймера генерируют прерывания.
С частотой 1мс происходит опрос состояний нажатия клавиш и передача Report-дескрипторов. Данное прерывание имеет приоритет выше. С частотой 10мс происходит чтение принятого дескриптора с состоянием Num/Caps/Scroll locks, подготовка буфера кадра для светодиодов и запуск ШИМ с DMA.
[UPD:]
3D модели тут https://www.thingiverse.com/thing:6451112
Печатная плата и прошивка тут https://github.com/LitLageR/keyboard
Проект BioHome3D реализован Университетом Мэна с использованием крупноформатного 3D-принтера компании Ingersoll. Здание выполнено из экологичных материалов — отходов деревообрабатывающей промышленности и биополимеров. Конструкция успешно пережила тяжелый год с заморозками до -17°С, жарой до 40°С и сильными бурями, вызвавшими веерные отключения электричества по всему штату.
Расходными материалами послужили древесные композиты со связующим в виде аморфного варианта полилактида. Этот полимер биоразлагаем, что зачастую интерпретируется как недолговечность, однако быстрое разложение полилактида требует определенных условий, например горячего компостирования. В любом случае, BioHome3D выдержал первый год эксплуатации без нареканий.
Площадь здания достигает пятидесяти шести квадратных метров, включая одну спальню и один санузел. Проект разработан в соответствии с программой строительства доступного жилья: на местном рынке недвижимости аналогичные, довольно скромные дома предлагаются за триста тысяч долларов и выше. Разработчики не рассчитывают, что 3D-печатные постройки полностью решат проблему, но надеются, что аддитивные технологии сделают свой вклад.
«Данные, собранные за год эксплуатации на открытом воздухе, доказали жизнеспособность технологии. Сейчас наша цель — масштабирование производственных процессов. С помощью «фабрики будущего» мы сможем производить по одному такому дому каждые сорок восемь часов. Мы построили дом, теперь пора застраивать кварталы», — прокомментировал исполнительный директор Центра продвинутых структур и композитов Университета Мэна Хабиб Дагер.
«Коробочка» здания выращена с помощью крупноформатной гибридной аддитивно-субтрактивной системы разработки компании Ingersoll, печатающей гранулированными полимерными композитами и способной выполнять механическую постобработку. Эта система считается одним из крупнейших экструзионных 3D-принтеров в мире. В 2019 году Университет Мэна установил сразу три рекорда Гиннесса, изготовив самый большой 3D-печатный катер, а по совместительству и самый большой цельный 3D-печатный объект, на самом большом экструзионном 3D-принтере.
В 2022 году Ingersoll совместно с компаниями MELD Manufacturing и Siemens взялась за разработку уже самого большого 3D-принтера для печати металлами — системы по технологии ротационной сварки трением, способной выращивать изделия длиной до десяти метров.
Китайцы выпустили гигантский 3D-принтер для дома, на котором можно печатать метровые штуки
OrangeStorm Giga умеет работать над четырьмя конструкциями одновременно и управляется с 7-дюймового съемного планшета. На максимальной скорости печатает 300 мм в секунду.
Коль уж тут выкладывают всякие интересные изделия личного производства, выложу и я своё творчество. Но что это такое, я вам не скажу. В процессе само станет понятно.
Итак, идея была в том, чтобы сделать собственный корпус, в котором будет жёстко закреплена плата с tpa3250 и блок питания для неё (в данном случае, от Mean Well, 60 Вт). Все детали печатаются на 3D-принтере из PLA.
Итак, собственно, первым делом спроектировал две плоские детали: разделительную стенку и днище. На стенке сделал возвышения для крепления основной платы.
Стенку обклеил металлизированным скотчем, который проводом соединил с землёй БП. Какое-никакое экранирование, хотя работает ли оно вообще, я не уверен.
В дне сделал крупную перфорацию. Чтобы была возможность забора холодного воздуха снизу. В инструкции к блоку сказано, что его нужно крепить строго вертикально. В принципе, логично. В таком случае, он будет охлаждаться конвекционными потоками, и эта же схема будет работать и с основной платой. Хотя на неё китайцы повесили какой-то совсем уж дохлый радиатор, но вроде, на нужных мне мощностях перегрева нет.
Далее, кожух. Заложил туда зазоры для передней и задней стенок, накрутил перфорацию, сделал ухо для единственного винта, на котором всё это непотребство будет держаться.
Самая дорогая и сложная в изготовлении деталь.
Далее, передняя и задняя панели. Нужно было предусмотреть места для крепления разъёмов: питания и входных-выходных сигналов, а также элементы управления: одну крутилку на потенциометре, кнопку отключения платы и переключатель питания сзади.
Также для передней панели предусмотрел некоторый элемент дизайна в виде декоративной решётки.
Блок питания рассчитан на крепление на din-рейку. Естественно, я пошёл самым кривым путём и сделал крепление типа "ласточкин хвост" на переднюю панель. А чтобы она не выпадала вперёд в процессе эксплуатации, я сделал в ней и в каркасе 2 паза под небольшой крючок. Когда основание вставляется в кожух, всё прижимается друг к другу и держится очень плотно.
Сзади, соответственно, всё уже попроще. Отдельно сделал плашку с информацией о том, кто, где и когда это всё разработал.
Сверху, как я уже и говорил, один единственный винт, держащий конструкцию. В случае чего, кожух выдвигается вперёд, спереди снимается внутренний крючок, и дальше открывается доступ ко всему, что есть внутри. По идее, пиковая механическая нагрузка происходит, если сильно трясти конструкцию в направлении выдвигания основания. В таком случае, при желании, это всё можно сломать. Но я так не делал, а в процессе повседневной эксплуатации и переноски, изделие не разрушилось.
По-хорошему, конечно, стоило использовать экранированные провода и более качественные радиодетали. Но оно, в принципе, и так работает.
В итоге, имеем дорогое и не особо надёжное устройство, тем не менее, исправно работающее, двух задолбанных коллег с 3D-принтерами и небольшой опыт работы в Solidworks.
Повторять это никому не рекомендую. Если вы такой же упоротый, как я, то сделаете по-своему и гораздо лучше. А если вам нужно практичное и дешёвое устройство, проще будет купить какую-нибудь металлическую коробку и разместить все компоненты в ней.
Ну и вопрос для тех, кто всё это прочитал. Что же это всё-таки за неведомая херня такая у меня получилась? Спасибо.
Когда выставил большую скорость, поленился откалибровать подачу филамента и пошёл спать.
Ожидание:
Реальность: