Grundsätzlich gibt es mehrere Ansätze dafür. Einmal geht sowas über
Interrupts (Timer, RTC) und zweitens über eine Funktion mit einer
Däumchendrehschleife.
Beim Däumchendrehen schreibst du dir eine Funktion und berechnest oder
misst, wie lange die Funktion braucht. Dabei justierst du bei der
Entwicklung bei gegebener Taktfrequenz solange Anzahl ms gegen
Schleifendurchläufe, bis die gewünschte Wartezeit erreicht ist. Diese
Kalibierung pro Taktfrequenz verwendest du dann im finalen Code.
Im allgemeinen hast du dabei eine Grundwartezeit für einen Aufruf der
Funktion plus Umrechnungen und einen konstanten Zusatzwert für jeden
zusätzlichen Schleifendurchlauf. In der Schleife selbst wird gerne der
kürzeste, nichtsmachende Assemblerbefehl "NOP" ausgeführt, damit die
Schleife nicht wegoptimiert wird.
Das Berechnen kannst du mit Kenntnis der Taktfrequenz des µC, dem
Assemblerlisting und dem Manual der Assemblerbefehle machen. Manche
Entwicklungsumgebungen haben Simulatortools mit Anzeige der
Ausführungszeit von Codeabschnitten.
Das Messen kannst du mit Porttoggeln und Monitoring des Ports auf einem
Qszilloskop oder Frequenzzähler machen.