MSP430 - UART / RS-232
Überblick
- Universal Asynchronous Receiver Transmitter
- Senden und Empfangen von Daten über jeweils eine Datenleitung
- RS-232 besitzt zusätzliche Steuerleitungen (z. B. Flow-Control: RTC, CTS)
- Über TXD und RXD werden Spannungssignale übertragen
RS-232: -12 V ... 12 V
UART: 0 ... 5 V oder 0 ... 3,3 V → abhängig von Betriebspannung der Mikrocontroller - Wandeln des UART-Signals in RS-232 durch IC MAX-232 (Datenblatt)
- Bitbreite: Bit/sec = Baud
- Typische Bitbreiten: 2400, 4800, 9600, 115200 Baud
Aufbauen einer UART-Verbindung mit dem MSP430
MSP430 Register
Detaillierte Beschreibung
Kalibieren des DCO
- Der DCO des MSP430 erzeugt den Takt für die MCLK und SMCLK.
- Dieser muss kalibriert werden, um exaktere Ausgangssignale zu liefern.
- Texas Instruments speichert Kalibrierwerte für 1, 8, 12 und 16 MHz im Flash-Speicher des MSP430.
- Der DCO des MSP430 ist nicht so exakt, wie der Quarzoszillator der ACLK.
- Die Frequenz des DCO ist außerdem stark abhängig von Umgebungstemperatur und Betriebsspannung.
- Jedoch ist das Timing besonders wichtig beim Senden und Empfangen von UART-Signalen.
Kalibieren des DCO
Auswahl der Pins
- Für UART werden P1.1 und P1.2 verwendet.
- Die PxSEL und PxSEL2 bits müssen entsprechend der Pinbelegung des MSP430G2553 gesetzt werden.
Pinbelegung des MSP430G2553
Verbinden der Pins P1.1 und P1.2 mit der UART-Schnittstelle
USCI Software Reset
- Das Software-Reset-Bit UCSWRST befindet sich im UCA0CTL1-Register.
- Während der Konfiguration des USCI-Moduls muss dieses Bit gesetzt sein.
- Nach der Konfiguration muss das Bit zurücksetzt werden, damit die Schnittstelle einwandfrei funktioniert.
USCI Software Reset
Konfigurieren des USCIA
- Wie in der UCA0-Registerbeschreibung zu sehen ist eine UART-Kommunikation der Standard (alle Bits sind null)
- keine Paritätsbit
- LSB zuerst
- 8-bit Daten
- 1 Stopbit
- UART
- Asynchrone Schnittstelle (keine Taktleitung)
- Im UCA0CTL1-Register muss nur die Taktquelle konfiguriert werden.
- Die SMCLK mit einer Taktrate von 1 MHz soll verwendet werden.
USCIA control register values
Setzen der Baudrate
- Das Setzen der Baudrate im MSP430 ist vergleichweise komplex. Es werden im MSP430 verschiedene Tricks verwendet, um ein möglichst exaktes Taktsignal zu generieren.
- Noch komplexer ist das Timing für den Datenempfang. Das Signal wird während einer Taktperiode mehrmals gesamplet.
- Aus diesem Grund muss die Tabelle mit Baudraten verwendet werden, um die korrekten Registerwerte zu konfigurieren.
- Die folgenden Werte können aus der Tabelle gelesen werden, für eine 9600 Baud-Signal bei einer Taktfrequenz von 1 MHz.
- Es ergeben sich daraus die Registerwerte wie folgt.
UCA0BR0
undUCA0BR1
.
mitUSCBRx = UCA0BR0 + 256 * UCA0BR1
UCA0MCTL = (UCBRFx << 4) + (UCBRSx << 1) + UCOS16
USCIA Timing-Einstellung für 9600 Baud bei 1 MHz
Sende eines Zeichens
- UART-Nachrichten werden Byte für Byte übertragen.
- Zum Senden einer Nachricht wird einfach das
UCA0TXBUF
register beschrieben. - Man muss sicherstellen, dass das Register niemals schneller befüllt wird, als das USCIA Daten versenden kann.
- Für das Beispielprogramm soll das Zeichen 'x' versendet werden, wenn der Taster an P1.3 gedrückt wurde.
- Daher ergibt sich die folgende ISR.
Senden eines Zeichens in der Port 1 ISR
#pragma vector=PORT1_VECTOR
__interrupt void PORT1_ISR() {
if (P1IFG & BIT3) {
P1IFG &= ~BIT3;
UCA0TXBUF = 'x';
}
}
Empfangen von Zeichen
- Für den UART-Datenempfang steht ein Interrupt zur Verfügung
- Um diesen Interrupt zu aktivieren, muss das folgende Bit gesetzt werden.
Aktivieren des RX-Interrupts
- Die folgende ISR wird verwendet, um die Nachricht zu dekodieren.
- Die zugehörige Interrupt-Flag
UCA0RXIFG
befindet sich im RegisterIFG2
. - Das empfangene Zeichen kann aus dem
UCA0RXBUF
-Register gelesen werden. - Die LEDs der RGB-LED sollen umgeschaltet werden, wenn die Zeichen r, g oder b empfangen wurden.
ISR für den Datenempfang - Umschalten der LEDs
#pragma vector=USCIAB0RX_VECTOR
__interrupt void USCI0RX_ISR(void) {
if (IFG2 & UCA0RXIFG) {
IFG2 &= ~UCA0RXIFG;
char received_char = UCA0RXBUF;
switch (received_char) {
case 'r':
P2OUT ^= BIT1;
break;
case 'g':
P2OUT ^= BIT3;
break;
case 'b':
P2OUT ^= BIT5;
break;
default:
break;
}
}
}
Low-Power-Mode
- Der Low-Power-Mode 4 kann in diesem Programm verwendet werden.
- Die SMCLK wird während des Senden- und Empfangens verwendet - dieser Systemtakt ist standardmäßig deaktiviert im LPM4.
- Jedoch kann der MSP430 automatisch die SMCLK aktivieren, wenn er zum Senden oder Empfangen benötigt wird. Der Taktgeber wird anschließend automatisch deaktiviert.
Programm
Erstes Programm für UART
#include <msp430.h>
#include <stdint.h>
int main(void) {
WDTCTL = WDTPW | WDTHOLD; // stop watchdog timer
BCSCTL1 = CALBC1_1MHZ;
DCOCTL = CALDCO_1MHZ;
P2OUT &= ~(BIT1 + BIT3 + BIT5);
P2DIR |= BIT1 + BIT3 + BIT5;
P1DIR &= ~BIT3;
P1OUT |= BIT3;
P1REN |= BIT3;
P1IES |= BIT3;
P1IFG &= ~BIT3;
P1IE |= BIT3;
P1SEL |= BIT1 + BIT2;
P1SEL2 |= BIT1 + BIT2;
UCA0CTL1 |= UCSWRST;
UCA0CTL0 = 0;
UCA0CTL1 |= UCSSEL_2;
UCA0BR0 = 6;
UCA0BR1 = 0;
UCA0MCTL = (8 << 4) + UCOS16;
UCA0CTL1 &= ~UCSWRST;
UC0IE |= UCA0RXIE;
__enable_interrupt();
while (1) {
__low_power_mode_4();
}
}
#pragma vector=USCIAB0RX_VECTOR
__interrupt void USCI0RX_ISR(void) {
if (IFG2 & UCA0RXIFG) {
IFG2 &= ~UCA0RXIFG;
char received_char = UCA0RXBUF;
switch (received_char) {
case 'r':
P2OUT ^= BIT1;
break;
case 'g':
P2OUT ^= BIT3;
break;
case 'b':
P2OUT ^= BIT5;
break;
default:
break;
}
}
}
#pragma vector=PORT1_VECTOR
__interrupt void PORT1_ISR() {
if (P1IFG & BIT3) {
P1IFG &= ~BIT3;
UCA0TXBUF = 'x';
}
}
Herstellen der UART-Verbindung mit dem PC
- PuTTY, siehe https://www.putty.org/
- HTerm, siehe http://www.der-hammer.info/pages/terminal.html
Kommunikation zwischen mehr als 2 Teilnehmern
Kommunikation zwischen mehr als 2 Teilnehmern in der Praxis eher selten.
- Verwendung mehrerer UART-Schnittstellen eines Mikrocontrollers
- Multiplexen der Datenleitungen
- Bit-Banging der UART-Schnittstelle
Senden von Zeichenketten und Zahlen
*Funktionen zum Senden von Zeichenketten und Zahlen
void uart_print(char *str)
{
while (*str != 0)
{
while (!(IFG2 & UCA0TXIFG))
{
}
UCA0TXBUF = *str;
*str++;
}
}
void uart_hex(uint8_t number)
{
char str[3];
for (uint8_t i = 0; i < 2; i++)
{
uint8_t digit = number & 0xf;
if (digit < 10)
{
str[1 - i] = '0' + digit;
}
else
{
str[1 - i] = 'a' + digit - 10;
}
number >>= 4;
}
str[2] = 0;
uart_print(str);
}
Beispiel-Implementierung eines Protokolls
Programmierung des MSP430
- Die Daten werden durch den RX-Interrupt aufgenommen und in einem Puffer abgespeichert.
- Nach einen Zeichenumbruch
\n
wird er LPM verlassen und der Puffer kann im Hauptprogramm ausgewertet werden. - Durch eine Nachricht in der Form
l000\n
/l111\n
können die RGB-LEDs entsprechend der gegebenen Bit-Werte einzeln an- oder ausgeschaltet werden. - Dazu werden verschiedene Variablen benötigt:
Variablen für den UART-RX-Puffer
UART-RX-ISR zum Dekodieren der Nachrichten
#pragma vector=USCIAB0RX_VECTOR
__interrupt void USCI0RX_ISR(void) {
if (IFG2 & UCA0RXIFG) {
IFG2 &= ~UCA0RXIFG;
char received_char = UCA0RXBUF;
uart_rx_buf[uart_rx_buf_pos] = received_char;
uart_rx_buf_pos++;
if (received_char == '\n') {
uart_rx_buf_len = uart_rx_buf_pos;
uart_rx_buf_pos = 0;
isr_flags |= BIT1;
__low_power_mode_off_on_exit();
}
}
}
- Der LPM kann in diesem Programm aus verschiedenen Gründen verlassen werden.
- Daher wird die Variable
isr_flags
benötigt.
Hauptprogramm mit Taster-Druck- und UART-RX-Event
while (1) {
if (isr_flags & BIT0) {
uart_send_string("btn\n");
isr_flags &= ~BIT0;
} else if (isr_flags & BIT1) {
message_decode();
isr_flags &= ~BIT1;
} else {
__low_power_mode_4();
}
}
Funktion message_decode() zum Dekodieren der empfangenen Nachricht und setzen der RGB-LEDs
void message_decode() {
if (uart_rx_buf[0] != 'l') {
return;
}
if (uart_rx_buf_len != 5) {
return;
}
if (uart_rx_buf[1] == '1') {
P2OUT |= BIT1;
} else {
P2OUT &= ~BIT1;
}
if (uart_rx_buf[2] == '1') {
P2OUT |= BIT3;
} else {
P2OUT &= ~BIT3;
}
if (uart_rx_buf[3] == '1') {
P2OUT |= BIT5;
} else {
P2OUT &= ~BIT5;
}
}
Gesamtes Programm
#include <msp430.h>
#include <stdint.h>
volatile char uart_rx_buf[16];
volatile uint8_t uart_rx_buf_pos;
volatile uint8_t uart_rx_buf_len;
volatile uint8_t isr_flags;
void uart_send_string(char *str);
void message_decode();
int main(void) {
WDTCTL = WDTPW | WDTHOLD; // stop watchdog timer
BCSCTL1 = CALBC1_1MHZ;
DCOCTL = CALDCO_1MHZ;
P2OUT &= ~(BIT1 + BIT3 + BIT5);
P2DIR |= BIT1 + BIT3 + BIT5;
P1DIR &= ~BIT3;
P1OUT |= BIT3;
P1REN |= BIT3;
P1IES |= BIT3;
P1IFG &= ~BIT3;
P1IE |= BIT3;
P1SEL |= BIT1 + BIT2;
P1SEL2 |= BIT1 + BIT2;
UCA0CTL1 |= UCSWRST;
UCA0CTL0 = 0;
UCA0CTL1 |= UCSSEL_2;
UCA0BR0 = 6;
UCA0BR1 = 0;
UCA0MCTL = (8 << 4) + UCOS16;
UCA0CTL1 &= ~UCSWRST;
UC0IE |= UCA0RXIE;
uart_rx_buf_pos = 0;
__enable_interrupt();
while (1) {
if (isr_flags & BIT0) {
uart_send_string("btn\n");
isr_flags &= ~BIT0;
} else if (isr_flags & BIT1) {
message_decode();
isr_flags &= ~BIT1;
} else {
__low_power_mode_4();
}
}
}
#pragma vector=USCIAB0RX_VECTOR
__interrupt void USCI0RX_ISR(void) {
if (IFG2 & UCA0RXIFG) {
IFG2 &= ~UCA0RXIFG;
char received_char = UCA0RXBUF;
uart_rx_buf[uart_rx_buf_pos] = received_char;
uart_rx_buf_pos++;
if (received_char == '\n') {
uart_rx_buf_len = uart_rx_buf_pos;
uart_rx_buf_pos = 0;
isr_flags |= BIT1;
__low_power_mode_off_on_exit();
}
}
}
#pragma vector=PORT1_VECTOR
__interrupt void PORT1_ISR() {
if (P1IFG & BIT3) {
P1IFG &= ~BIT3;
isr_flags |= BIT0;
__low_power_mode_off_on_exit();
}
}
void uart_send_string(char *str) {
while (*str != 0) {
while (!(IFG2 & UCA0TXIFG)) {
}
UCA0TXBUF = *str;
*str++;
}
}
void message_decode() {
if (uart_rx_buf[0] != 'l') {
return;
}
if (uart_rx_buf_len != 5) {
return;
}
if (uart_rx_buf[1] == '1') {
P2OUT |= BIT1;
} else {
P2OUT &= ~BIT1;
}
if (uart_rx_buf[2] == '1') {
P2OUT |= BIT3;
} else {
P2OUT &= ~BIT3;
}
if (uart_rx_buf[3] == '1') {
P2OUT |= BIT5;
} else {
P2OUT &= ~BIT5;
}
}
Programmierung des PC in Python
- Verwendete Python-Version: 3.8, siehe https://www.python.org/
- Benötigte Bibliothek: pyserial, siehe https://pypi.org/project/pyserial/
Installation über PIP:python -m pip install pyserial
- Verwenden der Funktion
input()
für Eingaben des Nutzers über die Befehlszeile, siehe https://docs.python.org/3/library/functions.html#input - Verwenden der Methode
serial.read_until(expected=serial.LF)
zum Einlesen von eingehenden Daten. - Beide Funktionen wirken blockierend! Daher wird ein zusätzlicher Thread erstellt.
- Verwenden des threading-Moduls: https://docs.python.org/3/library/threading.html
- Erstellen eines Events (thread-sicherer Boolean), das gesetzt wird, wenn das Programm beendet werden soll.
- Aufrufen der Funktion
serial_loop()
mit den passenden Argumenten in einen neuen Thread. serial.read_until()
liefertbytes
. Diese müssen in eine Zeichenkette dekodiert werden:
siehe https://docs.python.org/3/library/stdtypes.html#bytes- Zum Versenden der Nachricht müssen
bytes
übergeben werden. Daher wird eine Zeichenkette enkodiert. - Durch
if __name__ == "__main__":
wird in Python typischerweise das Hauptprogramm aufgerufen.
Empfangen der Daten im Beispielprotokoll
import threading
import serial
def main():
ser = serial.Serial("COM5", 9600, timeout=1)
stop_event = threading.Event()
threading.Thread(target=serial_loop, args=[ser, stop_event]).start()
while not stop_event.is_set():
inp = input("? ")
if inp == "x":
stop_event.set()
elif inp == "r":
serial_write(ser, "l100\n")
elif inp == "g":
serial_write(ser, "l010\n")
elif inp == "b":
serial_write(ser, "l001\n")
def serial_loop(ser, stop_event):
while not stop_event.is_set():
msg = ser.read_until(expected=serial.LF)
if len(msg) > 0:
msg = msg.decode("ascii")
print(msg[:-1])
def serial_write(ser, msg):
if isinstance(msg, str):
msg = msg.encode("ascii")
ser.write(msg)
if __name__ == "__main__":
main()