А туториал есть
Это кошку можно к лотку приучить, а человека уже намного сложнее. Если человек привык гадить - то для него это норма.
Делить на слои проект одной страницы - очень сложно. В том плане что самого кода мало, функций мало, и буквально всё связано. Обычно в таких одностраничных проектах - махание ножками распределяется на половину функций. И эти функции невозможно перевести в категорию "железо", потому как они ещё и логику периферии выполняют, а сверху логику основного цикла. В таких случаях проще пристрелить уродца, и переписывать с нуля.
Для начала надо написать простые функции работы с уровнем железа. При использовании хала, или чего-то подобного - такой набор функций уже есть. Значит пишем уровень логики периферии - те самые функции, что работают через железо с внешними интерфейсами. Можно в один файл, а можно файл под каждый тип логики. Они в любом случае пересекаться не должны. Уже сейчас вместо нескольких тысяч переменных и функций от чистого cmsis и hall - поле видимости будет включать только ваши функции и ваши данные. По сути у вас уже получился уровень драйвера, например функции записи чтения флеша по i2c шине.
Следующий уровень - логика общего алгоритма, это ещё не главный цикл, но очень близко к нему. Например функция моргания светиком при успешном обновлении данных во внешней памяти, работа со звуком, работа с экраном, и так далее. Полностью автономные функции, которые могут выполнять по настоящему полезную работу, не мешая другим функциям!!!
Кстати, некоторые сложные интерфейсы дотягиваются до этого уровня. Как например usb, инетнет, ваяфая, и кан. Они реально сложные, и писать их с нуля нет смысла - проще использовать готовое.
Следующий уровень системный - то что объединяет и разделяет весь написанный бардак в иерархию уровней доступа. Потому как очень многие вещи должны выполняться в строгой последовательности, иначе фигня получится. Писать заново рутинные последовательности каждый раз - максимально глупо. К таким вещам относятся файловые системы на разных носителях, алгоритмы работы с файфаем, простым радио, им так далее. И опять - подобное проще взять уже готовое.
Ну и самое важное - тот самый говонокод который будет всем этим зоопарком рулить - уровень пользователя, он-же главный цикл.
Например дождаться подключения флешкарты, запустить файловый интерфейс с переводом управления пользователю, и выполнять команды из главного цикла. Например воспроизведение звука, моргание светиком, махание лапками, и всё что душе угодно. Именно здесь используется самый верхний уровень, и всё ему подчиняется.
Некоторые простые функции можно пробрасывать между уровнями, но без вытягивания железа на самый верх. Нужно моргать светиком - напиши функцию на уровне железа, и используй сколько душе угодно. В таком случае при смене ветра или настроения - нужно будет переписать несколько строк на уровне железа ( допустим другая нога), или несколько строк в главном цикле. Но не писать весь проект с нуля, даже при переходе на другой тип мк!!!
И ещё, подготовка железа выполняется в функции SystemInit(), которая должна вызываться раньше main(). Многие рутинные вещи обязательно нужно перенести именно туда, чтоб не отсвечивали.