Using the on-chip Core Independent Peripherals (CIPs), the PIC18F56Q71 microcontroller can transmit Morse code (via an LED) or receive/decode Morse code (by printing to serial terminal). To implement this functionality, the Universial Timer (UTMR), Signal Routing Port, Configurable Logic Cell (CLC), Clock Reference (CLKREF), Numerically Controlled Oscillator (NCO), UART, and Timer 2 CIPs are used.
- MPLAB® X IDE v6.0.5 or newer
- MPLAB XC8 Compiler v2.41.0 or newer compiler
- MPLAB Code Configurator (MCC)
- MPLAB Data Visualizer or other serial terminal
- PIC18F-Q_DFP v1.17.379 or newer
This example is fully self-contained on the Curiosity Nano. However, an external button or telegraph key can be connected to RA1, for ease of use (if typing messages out manually).
I/O Pin | Function |
---|---|
RA0 | SW0 Input |
RA1 | External User Input |
RB4 | UART TX |
RB5 | UART RX |
RC7 | LED0 Output |
- Baud Rate: 9600
- Character Length: 8 bits
- Parity: None
- Stop Bits: 1 bit
This example can generate Morse code from a stream of text (A-Z, 0-9) as well as decode Morse code from a selectable input source, either the transmitter or an external input. The transmitter and receiver operate independently of each other.
In addition, the Configurable Logic Cells (CLCs) are used to implement a dual input debouncer and a digital multiplexer by using the Signal Routing Port.
Text to be transmitted is sent by UART to the microcontroller. When each character is received, the application loads it into a ring buffer. When the character '\n' is received, the transmission sequence begins.
In Morse code, dot and dash are differentiated by the length of their on time. After transmitting the dot or dash, the transmitter must remain off for a certain period of time. The length of the off time depends on whether this is a:
- In-Character Break
- Letter Break
- Word Break
For both simplicity and efficency, the state machine for the transmitter only is called when the transmitter state could change, as shown in the picture below, in red. Timer 2 is set up as a one-shot timer, with the period set by the state machine before starting. The output is assigned to both the LED and to a bit of the Signal Routing (SR) port peripheral.
This demo allows users to either use the button on the Curiosity Nano (SW0) or to connect their own button or input source. To implement this functionality, both inputs are connected via a NAND (in CLC4) to make a single output for the input debouncer.
The input debouncer utilized in this example is the 2 CLC configuration discussed in Switch Debouncing with the PIC18F16Q40.
For flexibility, this demo has two inputs. The default input is to connect the transmitter output to the receiver. Since the transmitter has tight timings, the receiver will always process the message correctly. The other input option is an external (debounced) input. This allows the user to decode their own entered message using the pushbutton on the Curiosity Nano or another signal source. To select between the two inputs, a bit from the SR Port is used. The SR port can be written to by the application like a normal I/O port.
To implement the multiplexer in the CLC, logic equivalent to the following is used.
Universal Timers (UTMR) are used to receive and decode Morse code. The UTMRs on this device are 16-bit timers with advanced start/stop/reset capabilities. When a rising edge occurs on the input to the UTMR, the timer starts and resets it's count. On the next edge (falling), the timer will stop and capture the current count. This generates a Capture Interrupt. Within a single UTMR, there are multiple Timer Units (TU).
If the pulse remains HIGH for longer than the timeout period set for the UTMR, the UTMR will reset to 0, and generate a PR Match Interrupt. In the PR Match Interrupt Service Routine (ISR), the UTMR is manually stopped to prevent more interrupts from being triggered. The UTMR is re-enabled shortly after, in the state machine.
Note: UTMR TU16B uses inverted logic to UTMR TU16A, but is otherwise identical.
Each interrupt from the UTMR is used to move the internal decoder state machine to the next state. In most cases, the sequence received would be:
- UTMR A - Capture
- UTMR B - Capture
In this case, the captured values from each of the UTMRs can be processed into the characters DOT / DASH / BREAK. For the positive width measurement (UTMR A), DOT and DASH are shifted into a buffer for later use, with a DOT being represented as 0 and a DASH being a 1.
For the negative width, DOT is in-character break (and is ignored), DASH indicates the end of a letter, which triggers the buffer to be decoded, and a DASH is considered a word BREAK, which decodes the buffer and then prints the word break.
When the Morse code is complete, UTMR B will timeout and generate a PR Match interrupt, as there is no rising edge to stop the timer.
During receive, if the timeout for the positive width measurement (UTMR A) is triggered, the state machine moves to an Error state. The negative edge measurement, when it occurs, will be discarded.
To transmit Morse code, type a message in the serial terminal, then press Enter. The message will not be sent until the microcontroller receives the '\n' character from the serial terminal. The transmitter output is LED0 on the Curiosity Nano. If the receiver is connected to the transmitter, then the output will also appear on the UART terminal. You can switch between user input and the internal transmitter by sending the '#' character when both transmitter and receiver are idle.
Note: Only characters A-Z and 0-9 are supported.
The receiver and decoder in this example have two possible inputs - an internal input, where the microcontroller decodes the same data that it transmits, or a user input, where either the pushbutton on the Curiosity Nano or an external button / telegraph key can be used to send data.
Note: By default, the program uses internal input.
Output Characters
'.' - Dot
'-' - Dash
'|' - Letter Break
'/' - Word Break (default)
'?' - Unable to decode character
Receiving and decoding is handled automatically by transmitting a message.
Note: The Morse code decoder requires specific timings for proper operation. There is some error margin provided in the decoder, but it is still difficult to consistently maintain the correct timing pattern.
To send a character, press and release the button with the following (default) timings:
- Timebase unit: 200 ms
- Dot (1 unit)
- Dash (3 units)
When the button is released, the length of the release time also plays a role in determining whether the character is complete or if there are more dots and dashes for the character being sent.
- In-Character Break (1 unit)
- Character Break (3 units)
- Word Break (7 units)
You can find a copy of the Morse code alphabet and specification here.
The receiver/decoder can be switched to manually keyed user input by uncommenting (USER_INPUT_DECODE
) in main.c
.
Note: At runtime, send the '#' character when both the transmitter and receiver are idle to switch.
If PRINT_ON_STARTUP
in main.c
is defined, a message such as "hello world" can be transmitted immediately on startup. The message can be changed in main.c
as well.
The timebase the Morse code transmitter and decoder runs at (MORSE_TIME_BASE
) can be adjusted in morseCommon.h
. The valid timebase range is between 40 ms and 1400 ms.
Note: To ensure correct operation with internal transmitter, timebase should be a multiple of 40.
If SHOW_LETTER_BREAKS
is commented out in morseRx.h
, then letter breaks will not be shown in the Morse code output.
Output Examples:
- With Letter Breaks: ...|---|...
- Without Letter Breaks: ...---...
The word break character can be changed to an arbitrary string in morseRx.h
by editing SPACE_CHAR_RX
. This must be a string.
If ENABLE_START_TX
is defined in morseCommon.h
, then the START symbol is transmitted before all messages.
This example has shown how the CIPs in the PIC18-Q71 family can be used to generate and decode data.