Привет, Хабровчане!
Это продолжение моего дневника разработки DIY струйного принтера. Предыдущие части:
DIY Open Source принтер. Часть 0
DIY Open Source принтер. Часть 1. Покоряем USB Printer Class и имитируем печать текста
В прошлый я рассказывал как наладить интерфейс между ПК и STM32 таким образом чтобы ПК понимал что мы подключили к нему принтер, а не неизвестное устройство. В этой статье я предлагаю разобраться с тем как работает печатающая голова в картридже HP123, каким образом сформировать управляющие сигналы для неё и как удержаться в рамках субмикросекундных таймингов.
Основным источником информации для меня выступают:
статья на Hackaday и проект PrintSpider_Arduino от нашего товарища по Хабру @lichnost
cтатья Magic Printer Cartridge Paintbrush и проект printercart_simple от Spritetm
Речь пойдёт только об отработке логики! Подключение железа пока невозможно - затронем в следующий раз - я работаю в этом направлении.
Содержание
§1. Базовая информация про HP123
§2. Добиваемся нужных таймингов на GPIO
§3. Формирование задания для HP123
§4. Работа над ошибками. Отладка проблемы при формировании задания
§5. Диаграммы здоровых управляющих сигналов

§1. Базовая информация про HP123
HP123 - это термоструйный картридж, который используется в ряде принтеров HP. Если кратко, то логика его работы заключается в том что рядом с дюзами имеется резистор, при подачи на него напряжения, он мгновенно нагревает чернила, создавая повышенное давление (тепловое расширение жидкости + пузырь из чернильного пара и, возможно, воздуха), из-за которого чернила выходят из дюз. Затем питание с резистора снимается, он охлаждается - давление понижается и втягивается новая порция чернил. Школьная физика в высоких технологиях! Немного подробнее можно прочитать тут.
Распиновка картриджа HP123 следующая:

Название контакта |
Описание |
Уровень напряжения |
DCLK |
Линия тактирования |
16 В |
CSYNC |
Переключение групп дюз |
9 В |
D1 |
Линия данных для жёлтого цвета. |
9 В |
D2 |
Линия данных для пурпурного |
9 В |
D3 |
Линия данных для синего |
9 В |
PWRA - F3 |
Питание линии A резисторов |
16 В |
PWRB - F5 |
Питание линии B резисторов |
16 В |
S1 |
Линия тактирования |
16 В |
S2 |
Линия тактирования |
16 В |
S3 |
Линия тактирования |
16 В |
S4 |
Линия тактирования |
16 В |
S5 |
Линия тактирования |
16 В |
GND |
Земля |
GND |
TS |
??? |
??? |
ID |
??? |
??? |
Возможно, ID передаёт идентификатор картриджа, чтобы принтер мог понять подключался ли картридж прежде, его оригинальность и т.д.
TS могло бы значить TimeStamp. Но на данный момент у меня нет предположений как и для чего это могло бы использоваться кроме как для обозначения что картридж подключён и работает (этакий heartbeat).
Согласно логам из статьи на Hackaday. DCLK должен отрабатывать с периодом около 0.4 мкс что составляет порядка 2.5 МГц.

Коротко о назначении сигналов (как я их понял) и их тайминги:
DCLK - тактирование. Период порядка 400 нс
CSYNC - переключение группы линий. Высокий уровень порядка 400 нс, видимо, период в 2 раза больше DCLK
D1-D2 - Data line. Каждая из которых обрабатывает данные для каждого цвета, возможно тактирование как у DCLK. Нечётные биты совпадают с фронтом DCLK, четные - с падением S1->S1->S3->S4->S5 (чередуются)
S1-S5 - Линии тактирование. Похоже что длительность высокого уровня на них близок к полному 1-1.5 периода DCLK - в логах, эти сигналы каждый раз выглядят немного по разному (неточности анализа?)
PWRA, PWRB - питание резисторов - не дольше 10 мкс высокого уровня
§2. Добиваемся нужных таймингов на GPIO
Я сконфигурировал ножки на цифровой вывод и стал переключать одну из них в цикле в цикле без явных задержек:
Код переключение GPIO
typedef enum {
PRINTHEAD_typeBlack,
PRINTHEAD_typeColor
} PRINTHEAD_tenHeadType;
/// @brief Print
typedef struct {
GPIO_TypeDef* port;
uint16_t pin;
} PRINTHEAD_tstHeadPin;
typedef struct {
PRINTHEAD_tenHeadType headType;
PRINTHEAD_tstHeadPin PWRA; // F3 - Print trigger line.
PRINTHEAD_tstHeadPin PWRB; // F5 - Print trigger line.
PRINTHEAD_tstHeadPin D1; // Data line for the yellow color.
PRINTHEAD_tstHeadPin D2; // Data line for the magenta color.
PRINTHEAD_tstHeadPin D3; // Data line for the color cyan. N/A for Black?
PRINTHEAD_tstHeadPin DCLK; // Сlock line.
PRINTHEAD_tstHeadPin CSYNC; // Data group switching line
PRINTHEAD_tstHeadPin S1; // Clock line.
PRINTHEAD_tstHeadPin S2; // Clock line.
PRINTHEAD_tstHeadPin S3; // Clock line.
PRINTHEAD_tstHeadPin S4; // Clock line.
PRINTHEAD_tstHeadPin S5; // Clock line.
PRINTHEAD_tstHeadPin ID; // ID - unknown how to use it
PRINTHEAD_tstHeadPin TS; // Unknown what it is
} PRINTHEAD_tstInstance;
void PRINTHEAD_Process(PRINTHEAD_tstInstance* head)
{
for (int i = 0; i < 10; i++)
{
HAL_GPIO_WritePin(head->D1.port, head->D1.pin, 0);
HAL_GPIO_WritePin(head->D1.port, head->D1.pin, 1);
}
}
Даже близко не то что нужно - 2 мкс (или 500 КГц) вместо 400 нс.

Попробуем воспользоваться более низкоуровневым управлением через BSRR регистр. Для этого я максимально освободил порт B и назначил GPIO следующим образом:
Пин |
Назначение |
PB1 |
S4 |
PB2 |
S5 |
PB3 |
PWRB |
PB4 |
PWRA |
PB5 |
D3 |
PB6 |
D2 |
PB7 |
D1 |
PB8 |
DCLK |
PB9 |
CSYNC |
PB13 |
S1 |
PB14 |
S2 |
PB15 |
S3 |
Текущая конфигурация GPIO портов в CubeMX

Для тех кому интересен расчёт и значений регистров PSC и ARP для таймера - спойлер ниже:
Расчёт регистров PSC и ARP для таймера
Нужно назначить таймер с прерыванием и DMA. Базовая формула для расчёта периода таймера:
или
Я хочу чтобы период таймера совпадал с полупериодом DCLK, т.е.:
f = 48 МГц = 48 * 106 Гц
T = 0.2 * 10-6 с
Если подставить мои значения в формулу:
Для простоты установим PSC = 0
и получится что ARP должен быть 8,6, но т.к. значение должно быть целочисленным, то примем ближайшее - APR = 9
.
Мой HCLK составляет 48 МГц и я могу воспользоваться одним из двух вариантов:
-
Передержать:
Prescaler (PSC) = 0
ARR (Period) = 9 ~208 нс.
-
Недодержать:
Prescaler (PSC) = 0
ARR (Period) = 8 ~190 нс.
Я выбрал вариант с 208 нс.
Устанавливаем DMA поток на TIM1_UP в направлении Memory To Peripheral. И (ВАЖНО!) размер данных Word, так как BSRR это 32 битный регистр и при каждом тике таймера в него будут помещаться новое значение.
Небольшая магия макросов поможет составить паттерн переключений (как на логах выше), значения которого будут отправляться в BSRR:
Макросы и тестовый паттерн для BSRR
// Положение пинов в регистре BSRR
#define PIN_S4 (1U << 1)
#define PIN_S5 (1U << 2)
#define PIN_PWRB (1U << 3)
#define PIN_PWRA (1U << 4)
#define PIN_D3 (1U << 5)
#define PIN_D2 (1U << 6)
#define PIN_D1 (1U << 7)
#define PIN_DCLK (1U << 8)
#define PIN_CSYNC (1U << 9)
#define PIN_S1 (1U << 13)
#define PIN_S2 (1U << 14)
#define PIN_S3 (1U << 15)
// Для одна половина регистра и��пользуется для включения пинов,
// другая - для сброса
#define PIN_SET(pin) ((uint32_t)(pin))
#define PIN_RESET(pin) ((uint32_t)((pin) << 16))
#define MakePatternPin(pin, value) (value ? PIN_SET(pin) : PIN_RESET(pin))
// Формирование одного uint32_t значения для паттерна
#define MakePatternLine(PWRA, PWRB, DCLK, CSYNC, D1, D2, D3, S1, S2, S3, S4, S5) \
MakePatternPin(PIN_PWRA, PWRA) | MakePatternPin(PIN_PWRB, PWRB) | \
MakePatternPin(PIN_DCLK, DCLK) | MakePatternPin(PIN_CSYNC, CSYNC) | \
MakePatternPin(PIN_D1, D1) | MakePatternPin(PIN_D2, D2) | MakePatternPin(PIN_D3, D3) | \
MakePatternPin(PIN_S1, S1) | MakePatternPin(PIN_S2, S2) | MakePatternPin(PIN_S3, S3) | MakePatternPin(PIN_S4, S4) | MakePatternPin(PIN_S5, S5)
#define PRINTHEAD__PatternsNumber 16
// Сам паттерн
static uint32_t PRINTHEAD__testPattern[PRINTHEAD__PatternsNumber] = {
// PWRA, PWRB, DCLK, CSYNC, D1, D2, D3, S1, S2, S3, S4, S5
MakePatternLine(0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0),
MakePatternLine(0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0),
MakePatternLine(0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0),
MakePatternLine(0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0),
MakePatternLine(0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0),
MakePatternLine(0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0),
MakePatternLine(0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0),
MakePatternLine(0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0),
MakePatternLine(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1),
MakePatternLine(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1),
MakePatternLine(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
MakePatternLine(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
MakePatternLine(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
MakePatternLine(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
MakePatternLine(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
MakePatternLine(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
};
Функция, которая кладёт одну строку из паттерна в BSRR по индексу PRINTHEAD__currentPatternNumber
:
(ВНИМАНИЕ!!! Тут закралась ошибка, которую я буду анализировать в §4.)
Отправка паттерна в BSRR и запуск таймера
void Start_Print_DMA(void)
{
if (htim1.State != HAL_TIM_STATE_READY) {
return; // таймер ещё занят
}
if (HAL_DMA_Start_IT(&hdma_tim1_up,
// ОШИБКА! PRINTHEAD__currentPatternNumber должен быть удалён
(uint32_t)&(PRINTHEAD__patterns[PRINTHEAD__currentPatternNumber]),
(uint32_t)&GPIOB->BSRR,
PATTERN_LEN) != HAL_OK) // ОШИБКА! должен быть sizeof(PRINTHEAD__patterns)
{
LOG_Error("%s", "DMA Start error");
}
__HAL_TIM_ENABLE_DMA(&htim1, TIM_DMA_UPDATE);
if (HAL_TIM_Base_Start(&htim1) != HAL_OK)
{
LOG_Error("%s", "Timer Start error");
return;
}
}
Далее два важных момента: переключение строки паттерна и передача следующего должны происходить максимально быстро. Для этого можно воспользоваться двумя событиями таймера:
HAL_DMA_XFER_CPLT_CB_ID - срабатывает при тике таймера - период
HAL_DMA_XFER_HALFCPLT_CB_ID - срабатывает на середине тика таймера - полупериод
Первый используем чтобы освободить DMA поток, а второй чтобы переключить строку паттерна и, возможно, остановить печать. Минимальная реализация функций обратного вызова для DMA ниже:
(ВНИМАНИЕ!!! Тут тоже закралась ошибка - тогда я ещё не до конца понял как работает связка таймера и BSRR. Проблема будет решаться в §4.)
DMA Callbacks
static void PRINTHEAD__TimerDMACallback(DMA_HandleTypeDef *hdma)
{
if (hdma == &hdma_tim1_up) {
HAL_TIM_Base_Stop(&htim1);
__HAL_TIM_DISABLE_DMA(&htim1, TIM_DMA_UPDATE);
// Сбросить состояние DMA
HAL_DMA_Abort(hdma); // ОШИБКА! Достаточно сбросить событие и остановить таймер. Вероятно, именно из-за этой строчки и требовался перезапуск DMA
// ОШИБКА! Не надо перезапускать DMA, он сам переключится на следующее значение
// Теперь можно подготовить новый буфер и снова вызвать Start_Print_DMA()
if (PRINTHEAD__isStarted) Start_Print_DMA();
}
}
static void PRINTHEAD__HalfTimerDMACallback(DMA_HandleTypeDef *hdma)
{
if (hdma == &hdma_tim1_up) {
// ОШИБКА! всё это лишние операции
PRINTHEAD__currentPatternNumber++;
if (PRINTHEAD__currentPatternNumber >= PRINTHEAD__PatternsNumber)
{
PRINTHEAD_Stop();
PRINTHEAD__currentPatternNumber = 0;
}
}
}
void PRINTHEAD_Init(PRINTHEAD_tstInstance* head, PRINTHEAD_tenHeadType headType)
{
...
HAL_DMA_RegisterCallback(&hdma_tim1_up, HAL_DMA_XFER_CPLT_CB_ID, PRINTHEAD__TimerDMACallback);
HAL_DMA_RegisterCallback(&hdma_tim1_up, HAL_DMA_XFER_HALFCPLT_CB_ID, PRINTHEAD__HalfTimerDMACallback);
...
}
При запуске получаем следующую картину:

DCLK - период 417 нс;
CSYNC - длительность 375 нс (я ждал 417 нс)
S1..S5 - длительность каждого 417 нс
Суммарная длительность группы ~2 мкс.
У моего анализатора только 8 каналов, так что D1,D2 и PWRA,PWRB пока что остались ни у дел.
С этим уже можно работать! Для более точного попадания в 400 нс (если это будет необходимо) придётся поработать с деревом тактирования. Вместо 48 МГц HCLK должен быть кратен требуемой частоте, в нашем случае, ближайшие значения это 45 и 50, но удобнее работать с 40 или 80. Однако, в таком случаем можно забыть про работы USB (он тактируется от 48 КГц) - этим путём можно будет пойти в крайнем случае (уже представляю схему из 3xSTM32F103 или 2xSTM32F103 + 1xSTM32F411).
§3. Формирование задания для HP123
Когда мы убедились что у нас получится выдержать необходимые тайминги, можно приступать к настоящему управлению картриджем.
Для начала зададим простую структуру для обозначения цвета пикселя. Напомню, картриджи работают в кодировке CMYK где каждый цвет занимает 1 байт. Объединить её с uint32_t
значением может оказаться хорошей идеей для быстрого доступа:
/*
PIXEL_CMYK pix;
pix.value = 0xAABBCCDD;
uint8_t c = pix.C; // 0xDD
uint8_t m = pix.M; // 0xCC
uint8_t y = pix.Y; // 0xBB
uint8_t k = pix.K; // 0xAA
*/
typedef union {
struct {
uint8_t C;
uint8_t M;
uint8_t Y;
uint8_t K;
} CMYK;
uint32_t value;;
} PRINTHEAD_tunColor;
А теперь будем разбираться с библиотекой в проекте PrintSpider и printercart_simple как её первоисточника.
Начнём с того чёрного картриджа и будем пытаться залить всё чёрным. Для этого я создал массив на 600 элементов (размер ничем не обоснован) и для которого буду подготавливать данные снаружи:
Объявление паттерна для переключения GPIO
#define PRINTHEAD__PatternsNumber 600
/*
Each line durations is equal timer duration.
Timer duration is based on the fastes signal - DCLK.
DCLK perion = 400 ns -> Timer period = 200 ns
*/
static uint32_t PRINTHEAD__patterns[PRINTHEAD__PatternsNumber] = {0};
static uint8_t PRINTHEAD__currentPatternNumber = 0;
В потоке RTOS, который контролирует процесс печати я вызову один раз PRINTHEAD_SetLineColor
, в которой забью PRINTHEAD__patterns
значениями для заливки сплошной заливки чёрным. И буду периодически перезапускать печать одной и этой импровизированной строки (ещё помните что она останавливается в полупериоде таймера?). Дальше дело будет за таймером и BSRR.
Интерфейс для заполнения паттерна
В потоке PRINTCTRL
"отправляем строку на печать" и запускаем её через PRINTHEAD_Start
:
void PRINTHEAD_SetLineColor(PRINTHEAD_tstInstance* head, PRINTHEAD_tunColor color);
void PRINTCTRL_Task(void *pvParameters)
{
PRINTHEAD_tunColor color = {0};
PRINTHEAD_SetLineColor(&PRINTCTRL__blackHead, color);
PRINTHEAD_Start();
for(;;)
{
if (!PRINTHEAD_IsStarted())
{
PRINTHEAD_Start();
}
osDelay(1);
}
}
В чём, собственно состоит подготовка?
Чёрный картридж HP123 содержит 2 линии по 168 сопел, 2 первых и последних не соединены -> 324 сопла в сумме. Для каждого необходимо установить некоторое значение для выброса чернил. Я позаимствую библиотечную часть кода и основную логику подготовки из проекта PrintSpider_Arduino. Таким образом подготовка сведётся к трём этапам.
Этап 1. Подготовка данных для каждого сопла
Вызываем printspider_set_nozzle_black
и передаём ему полученный снаружи цвет (color = 0x00000000
) вместе с координатами сопла:
PRINTHEAD_SetLineColor - подготовка данных для каж
Перенос логики из send_image_row_black в PrintSpider
void PRINTHEAD_SetLineColor(PRINTHEAD_tstInstance* head, PRINTHEAD_tunColor color)
{
uint8_t nozdata[PRINTSPIDER_NOZDATA_SZ];
memset(nozdata, 0, sizeof(nozdata));
for (int row = 0; row < 2; row++) {
for (int y = 0; y < PRINTSPIDER_BLACK_NOZZLES_IN_ROW; y++) {
// We take anything but white in any color channel of the image to
// mean we want black there.
switch (head->headType)
{
case PRINTHEAD_typeBlack:
{
if (color.CMYK.K != 0xff) {
// Random-dither 50%, as firing all nozzles is a bit hard on the
// power supply.
// if (rand() & 1) {
printspider_set_nozzle_black(nozdata, y, row);
// }
}
break;
}
case PRINTHEAD_typeColor:
{
/* code */
break;
}
default:
LOG_Warning("Not Supported head type: %d", head->headType);
break;
}
}
}
PRINTHEAD__prepareDataOut(nozdata);
}
И переходим к следующему этапу с PRINTHEAD__prepareDataOut
.
Этап 2. Формируем команды для картриджа
На базе данных с предыдущего сигнала формируем массив значений, которые необходимо будет воспроизвести на GPIO:
PRINTHEAD__prepareDataOut - формирование управляющих сигналов
Немного адаптированный с send_nozdata_out из PrintSpider_Arduino
#define WAVEFORM_LEN 600
static void PRINTHEAD__prepareDataOut(uint8_t *nozdata)
{
static uint16_t waveform_buffer[WAVEFORM_LEN];
memset(waveform_buffer, 0, sizeof(uint16_t) * WAVEFORM_LEN);
int generated_len = printspider_generate_waveform(waveform_buffer, selected_waveform.data, nozdata, selected_waveform.len);
PRINTHEAD__preparePins(waveform_buffer, generated_len);
}
Этап 3. Формируем паттерн для BSRR
Транслируем printspider массив сигналов на свои GPIO:
PRINTHEAD__preparePins - готовим паттерн, который можно отправить в BSRR
Опираясь на объявление пинов в PrintSpider_Arduino вышло это:
static void PRINTHEAD__preparePins(uint16_t *buffer, int len)
{
static struct{
bool F3;
bool F5;
bool DCLK;
bool CSYNC;
bool D1;
bool D2;
bool D3;
bool S1;
bool S2;
bool S3;
bool S4;
bool S5;
} P;
for (int i = 0; i < len && i < PRINTHEAD__PatternsNumber; i++) {
uint16_t buffer_i = buffer[i];
P.F3 = buffer_i >> 10 & 1;
P.F5 = buffer_i >> 11 & 1;
P.DCLK = buffer_i >> 8 & 1;
P.CSYNC = buffer_i >> 3 & 1;
P.D1 = buffer_i >> 0 & 1;
P.D2 = buffer_i >> 1 & 1;
P.D3 = buffer_i >> 2 & 1;
P.S1 = buffer_i >> 6 & 1;
P.S2 = buffer_i >> 4 & 1;
P.S3 = buffer_i >> 9 & 1;
P.S4 = buffer_i >> 5 & 1;
P.S5 = buffer_i >> 7 & 1;
uint32_t patternLine = MakePatternLine(
P.F3,
P.F5,
P.DCLK,
P.CSYNC,
P.D1, P.D2, P.D3,
P.S1, P.S2, P.S3, P.S4, P.S5
);
PRINTHEAD__patterns[i] = patternLine;
}
}
Ожидание, получить нечто, похожее на что-то напоминающее тестовый паттерн. Я ожидал увидеть тот же паттерн что и в тесте, но получил что-то странное:

Паттерн из PrintSpider_Adrduino
Для сравнения, паттерн из PrintSpider_Adrduino, который Я запустил на Arduino Nano c той же заливкой чёрным:

Даже если не брать во внимание плавающий период DCLK (порядка 500-750 нс), логика сигналов сильно нарушена. На крупном масштабе можно рассмотреть что-то похожее на правильную структуру

Поскольку я считаю данный цикл статей чем-то вроде дневника разработки, в следующем параграфе я буду анализировать эту проблему и исправлять свои ошибку - это не самая интересная часть, так как в ней, я искал иголку в стоге сена (как мне казалось), которая оказалась размером с этот самый стог - небольшой урок внимательности.
§4. Работа над ошибками. Отладка проблемы при формировании задания
Проверим что происходит на первом этапе. Для Arduino инициализируем UART и напишем незатейлевый вывод в конце этапа. Для STM32 достаточно точки останова и вывода в watch. Как это ни удивительно, но разницы абсолютно никакой. Массивы заполнены 0xFF
:

Применив подобный ход ко второму этапу, я получил абсолютно одинаковые числа:

Проверим третий этап. Я собрал значения в PRINTHEAD__testPattern
в таблицу и вывел несколько интересующих меня диаграмм для первых 280 строк (я так и не понял может ли LibreOffice Calc разделить значения на несколько осей X):

Фрагмент исходных данных для графика

Они достаточно серьёзно отличаются от того что показывает логический анализатор. Но можно с определённой уверенностью сказать что паттерн для GPIO сформирован правильно. Возможно, проблема кроется в переключениях паттерна, однако, тестовый паттерн был стабилен, несмотря на то что работал на более высоких таймингах.
После я заметил очевидную ошибку в счётчике паттернов. Его размер составлял всего 1 байт, что критично с учётом 517 реальных значений. После фикса я не увидел ничего нового на анализаторе.
static uint8_t PRINTHEAD__currentPatternNumber = 0; // Было
static uint8_t PRINTHEAD__currentPatternNumber = 0; // Стало
Я проверил непосредственно функцию Start_Print_DMA и не увидел никаких ошибок DMA, а если разложить uint32_t
значение обратно на составляющие GPIO, то строки паттерна всё ещё остаются логичными и сходятся с задуманным паттерном.
Что известно по этой ошибке сейчас:
PrintSpider библиотека формирует правильные данные
DMA свободен и не возвращает ошибок
В регистр действительно отправляются те данные, которые должны
После долгих мучений я нашёл корень проблемы. Я забыл о принципе работы функции HAL_DMA_Start_IT
и что последним элементом должен передаваться размер всего паттерна, а не его отдельная часть (во время тестов там находилась длина тестового паттерна и я это упустил).
Работа связки таймера с BSR тоже была некорректной. Меня с самого начала напрягало диагональное движение сигналов на диаграмме, я чувствовал что абсолютно никакой необходимости считать на каком элементе в данный момент находится таймер и, уж, тем более, возобновлять отправку паттерна. Что ж, будет мне уроком что надо быть внимательнее!
В итоге, управление GPIO свелось к нескольким функциям, которые приняли следующий вид:
Управление GPIO
void PRINTHEAD__PrintPattern()
{
if (htim1.State != HAL_TIM_STATE_READY) {
return; // таймер ещё занят
}
if (HAL_DMA_Start_IT(&hdma_tim1_up,
(uint32_t)PRINTHEAD__patterns,
(uint32_t)&GPIOB->BSRR,
PRINTHEAD__PatternsNumber) != HAL_OK)
{
LOG_Error("%s", "DMA Start error");
}
__HAL_TIM_ENABLE_DMA(&htim1, TIM_DMA_UPDATE);
if (HAL_TIM_Base_Start(&htim1) != HAL_OK)
{
LOG_Error("%s", "Timer Start error");
return;
}
}
static void PRINTHEAD__TimerDMACallback(DMA_HandleTypeDef *hdma)
{
if (hdma == &hdma_tim1_up) {
__HAL_TIM_DISABLE_DMA(&htim1, TIM_DMA_UPDATE);
HAL_TIM_Base_Stop(&htim1);
if (!PRINTHEAD__isStarted)
{
HAL_DMA_Abort(&hdma_tim1_up);
}
}
}
static void PRINTHEAD__HalfTimerDMACallback(DMA_HandleTypeDef *hdma)
{
if (hdma == &hdma_tim1_up) {
if (!PRINTHEAD__isStarted)
{
/*
TODO: нужна нормальная нормальная обработка остановки печати:
- безопасность для картриджа (сброс PWRA/PWRB)
- обнуление группы сопел
*/
memset(PRINTHEAD__patterns, 0, sizeof(PRINTHEAD__patterns));
}
}
}
§5. Диаграммы здоровых управляющих сигналов
В итоге я увидел то что больше похожее на на ожидаемое:

Период DCLK всё ещё плавает (750-1000 нс). Однако, расстояние между последним срезом DCLK в группе и первым фронтом DCLK следующей группы составляет 4.5 мкс, что близко к тому о чём писал Spritetm(4 мкс).
Обратим внимание на PWRA и PWRB. Они включаются на 1.25 мкс - это вписывается в требование: "не более 10 мкс":

Я устанавливал ARP = 4, но, это не дало ощутимого прироста. По всей видимости я уперся в передел производительности контроллера на данной частоте.
Нужно поближе присмотреться к функции PrintSpider, формирующей паттерн - она дублирует строки (это видно по предыдущему параграфу), вероятно, это было связано с большой частотой I2S, который использовался в ESP32, и период взят с двойным запасом. Кроме того, необходимо оптимизировать функции обратных вызовов, они также могут вносить значительный вклад в нарушение таймингов. Поближе ознакомлюсь с тем что было в проекте на ESP32.
Описанный в данной статье базовый функционал нуждается в доработке, но он уже влит в главную ветку репозитория проекта и будет полироваться дальше с учётом формирующейся архитектуры. Самое главное - логика выглядит работающей, возможно, погоня за таймингами не стоит свеч, ведь если DCLK и S1-S5 действительно тактируют картридж, то даже текущих скоростей более чем достаточно.
Заключение
В текущей конфигурации не получится подключить два картриджа сразу, но, пока что одного чёрного хватит для продолжения.
Вполне реально освободить немного места на порту A для цветного картриджа. Позже я попробую добавить функционал для работы с ним (он не сильно отличается от чёрного) и, возможно опубликую это в виде мини-статьи или поста/новости (это ведь подходит под их формат?).
Теперь можно будет вплотную взяться за электронную составляющую!
@lichnost действительно отправил мне несколько своих плат для тестов - огромный респект ему за это! На момент написания статьи, они пока ещё в пути.
Уже известно что схема защиты для PWRA/PWRB работает некорректно. Можно её обойти ради "проверки на дым", но анализатор показывает долгие высокие на уровни во время SoftReset контроллера и, как следствие, во время прошивки - это летально для печатающей головы. Так что для начала стоит отыскать решение этой проблемы.
Параллельно с проблемой схемы защиты, нужно решить вопрос организации питания. Необходимо минимум 3 линии питания:
5В питания для STM32 и прочей логики
9 В + 16 В для работы картриджа
9 В для двигателей, которые будут использоваться ещё не скоро.
В идеале, нужно развести плату блока питания, но мне нравится мысль, купить компьютерный блок питания по цене семечек (или найти в закромах у знакомых) и иметь мощные 5 и 12 В для преобразования.
По традиции - послесловие
Т.к. есть подозрения на упор в производительность, то появилась шальная мысль, установить кварц на 24 МГц разогнать SYSCLK до 96 МГц (очень близко к пределу в 100 МГц). Из того что на поверхности - это увеличит производительность (важно для обработки всех прерываний) и даст немного больше простора для работы таймера, но также увеличит энергопотребление (что не важно сейчас, но может оказаться актуальным в законченном устройстве) и чувствительность к просадкам на шине питания, так же есть вероятность понизить общую стабильность работы микроконтроллера.
Решение проблемы схемы защиты PWRA/PWRB у меня это займёт какое-то время (я ещё озадачу сведущего в схемотехнике человека, но тем ни менее), поэтому, возможно, следующие статьи могут оказаться не по запуску картриджа, а, например, про написание драйвера с добиванием отправки изображений что, думаю, будет интересно многим на Хабре, или же новые части будут посвящены текущей архитектуре проекта и подробностям устройства модулей, CI/CD, PVS-Studio и т.д. - чисто для поддержания пульса проекта на Хабре и в качестве небольшой передышки для меня :D (+ наработки опыта в написании статей)
Всем спасибо за внимание! До следующей части!
vigfam
Вероятно, вполне годное решение. Я когда-то пробовал разгонять пожилые(~2012 года) stm32f407 - для на коленке сбацанного рефлектометра. Разгон (при питании 3.3v) с 168 до ~210 два имевшихся экземпляра переживали абсолютно безболезненно, при разгоне >~230 начинала забавно отваливаться периферия - таймеры, FSMC, DMA с их логикой и триггерами.