... ob es möglich ist, UART ... möglichst gut auszunutzen ... CrLf weglässt und stattdessen wirklich jedes übertragene Byte einzeln auswertet ...
Ich weiß nicht, ob ich das jetzt richtig verstehe. Meine UART-Telegramme sind - bis auf Textausgaben an Terminal - eigentlich immer ohne CR/LF. Mal Beispiele.

Befehle per UART sind bei mir meist Steuerbefehle zu Testzwecken von PC nach Controller aber eben auch einzelne Controller-Controller-Kommunikation (der 1284 hat praktischerweise zwei UARTs). So ein Telegramm hat dann allerlei Inhalt - aber praktisch nie ein CrLF (Ausnahme die Terminalmeldungen) - es wird allerdings der gesendete String mit einer 0x00 abgeschlossen. Der Befehlsempfänger weiß dann, wann welcher Wert beginnt und wann er aufhört. Die Telegramme sind dabei recht kurz, derzeit deutlich unter 14 Bytes (einschließlich er String-Null). Soweit die Übertragung von Zeichen und Zahlen per ASCII-String. Mal ein aktuelles Beispiel vom Kopfcontroller meines Archie.
Code:
  #define   KOMMANDO_APPS       'A'     // Anwendungsprogramme fahren, diverse
  #define   KOMMANDO_APPS_LEN     7     //   Annkkkk
                                        //    nn =:   01..99 - Programmnummer
                                        //      kkkk  Parameter für Prog

  #define   KOMMANDO_DATS       'D'     // (aktuelle) Daten der Servos
  #define   KOMMANDO_DATS_LEN     1     // Kommandolänge

  #define   KOMMANDO_NORM       'N'     // Normposition der Servos anfahren
  #define   KOMMANDO_NORM_LEN     1     // Kommandolänge

  #define   KOMMANDO_OFFA       'O'     // Offset anzeigen für akt Servo x
  #define   KOMMANDO_OFFA_LEN     2     // Kommandolänge

  #define   KOMMANDO_OFFC       'o'     // Offset ändern (Change) für akt Servo x
  #define   KOMMANDO_OFFC_LEN     5     // Kommandolänge
                                // bei ox333 => akt. Wert ins EEPROM schreiben

  #define   KOMMANDO_POSER      'P'     // Positionierbefehl Servo
  #define   KOMMANDO_POSER_LEN    6     //   Pn3600 {1-9, j, z} 10 Servo
                                        //    n =:  1..9, j=10, z=alle
  #define   klz                 122     //   kleines "z"
  #define   GRZ                  90     //   Grosses "Z"

  #define   KOMMANDO_WICHTIG    'W'     // Wichtiger Befehl
  #define   KOMMANDO_WICHTIG_LEN  1

  #define   KOMMANDO_TEST       'T'     // Test x {0 .. 9} aufrufen
  #define   KOMMANDO_TEST_LEN     2     //   .. in do_test/r4n

  #define   KOMMANDO_VOICE      'V'     // Audioausgabe aufrufen
  #define   KOMMANDO_VOICE_LEN    4     //   .. in ~aud~/plyWTV

  #define   KOMMANDO_PC_CON     'Z'     // Zurück zur Console
  #define   KOMMANDO_PC_CON_LEN   2

//      Muss nur das laengste Kommando aufnehmen koennen.
//#define   KOMMANDO_BUFFER_LAENGE   14         // ##>> Deklaration => ~com~
//  eigener BUFFER-Speicher um aus dem 'unsichtbaren' FIFO-Speicher umzukopieren
//u8 mein_rx_buff [KOMMANDO_BUFFER_LAENGE];     // ##>> Deklaration => ~com~
Der rot markierte String ist hier nur EIN Beispiel für einen tatsächlich gesendete/empfangene String OHNE String-Ende-Marke. Der bedeutet in diesem Beispiel
Fahre Position von Servo n auf den Wert 3600. Anm.: Ich stelle meine analogen Servos mit einem Puls der fast 14 Bit fein codierbar ist.
Zu sehen ist hier auch deutlich, dass die Länge des Befehlsstrings vom Kommandoanfang codiert wird. Das hat nicht zuletzt den Hintergrund mir selbst eine Eselsbrücke bein Programmieren und beim Testen zu bauen.

Solche Strings sendet auch in manchen Situationen per UART der Master an Slaves.

I²C: Werte mit ein oder zwei Bytes übertrage ich häufig, insbes. per I²C - und dann als "richtige" Integerwerte mit ein oder zwei Bytes. Da muss der Befehlsempfänger eben wissen wie er die verschiedenen Bytes auszuwerten hat. Das löse ich dadurch, dass z.B. eine Dekade Pufferbytes für Motorenwerte benutzt wird. Byte x9 ist das Statusbyte - ist das auf 85 so der Puffer bereit zur Aufnahme eines neuen Befehls. Dann schreibt der Master seine z.B. zwei Motorstellwerte, z.B. 240 / 250 in die Pufferzellen 52-53 und 54-55, in die 59 kommt ne 99 als Zeichen, dass neue Daten im Puffer stehen. Der Slave liest das und quittiert mit ner 85 auf Byte 59.

Ist das der Hintergrund der Frage?