IMX6ULL UART Serial Communication Practice
I. Core Concepts of UART
Before writing code, let's clarify the core concepts of embedded communication, which form the foundation for understanding UART operation principles.
1. Communication
In embedded systems, communication essentially means data interaction between two or more hosts. UART is one type of asynchronous serial communication method that doesn't require a clock line for synchronization. It only needs TX (transmit) and RX (receive) lines to achieve bidirectional data transfer.
2. Asynchronous vs. Synchronous
- Asynchronous communication: No dedicated clock line is needed. Both parties agree on parameters like baud rate, data bits, and stop bits, achieving synchronization through a frame format of "start bit + data bits + parity bit + stop bit" (UART belongs to this category).
- Synchronous communication: Requires a clock line (e.g., SCK in SPI, SCL in I2C). Both parties strictly synchronize with the clock signal, offering higher transmission efficiency (e.g., SPI, I2C).
3. Serial vs. Parallel
- Serial communication: Data is transmitted bit by bit over a single line (e.g., UART, RS485). Advantages include simple wiring and long transmission distance, while the disadvantage is relatively slower speed.
- Parallel communication: Multiple bits of data are transmitted simultaneously over multiple lines (e.g., MCU parallel bus). Advantages include high speed, while disadvantages include complex wiring, poor anti-interference, and short transmission distance.
4. Simplex/Half-Duplex/Full-Duplex
- Simplex: Supports one-way transmission only (e.g., remote control → TV).
- Half-duplex: Supports bidirectional transmission but only one party can send at a time (e.g., walkie-talkie).
- Full-duplex: Supports simultaneous bidirectional transmission (UART defaults to full-duplex with independent TX/RX).
5. TTL/RS232/RS485
These are different level standards designed for different transmission scenarios:
- TTL: Native level for embedded chips (high level ≈ 3.3V/5V, low level ≈ 0V), with short transmission distance (<1 meter).
- RS232: Negative level standard (high level -3~-15V, low level +3~+15V), with a transmission distance of ≈15 meters.
- RS485: Differential level transmission with strong anti-interference, supporting transmission distances up to kilometers and multi-device communication.
6. Differential Transmission
Differential transmission uses voltage difference between two signal lines to represent logic levels (e.g., A-B > 0.2V for logic 1, A-B < -0.2V for logic 0). It effectively cancels common-mode interference (e.g., power line noise, electromagnetic interference) and is the core technology for long-distance communication like RS485 and CAN.
II. Hardware Schematic Analysis (IMX6ULL_MINI_V2.2)
This practice is based on the "USB USART & USB POWER" module in the schematic "IMX6ULL_MINI_V2.2(Mini Board Schematic).pdf". The core hardware components are as follows:
1. Core Module Breakdown
| Module | Description |
|---|---|
| USB_TTL Port | Acts as the physical interface for UART, connecting the computer USB to the CH340 chip |
| CH340 (U8) | USB-to-TTL serial chip, converting "computer USB bus" to "TTL-level UART" on the board |
| DCDC (U12/U13) | Power regulation module, providing stable power with anti-jitter and anti-interference (⚠️ Not recommended to power via USB due to overheating risks) |
2. Hardware Logic
Computer connects to the board's USB_TTL port via USB cable → CH340 converts USB signals to TTL-level UART signals → Connects to IMX6ULL's UART1_TX/RX pins → Enables "computer-board" UART communication.
III. UART Code Implementation (Based on IMX6ULL)
Code development refers to "IMX6ULL Reference Manual.pdf" and is divided into five steps: clock initialization , pin initialization , register configuration , transceiver function implementation , and stdio porting.
1. Preparations
First, define the base address of IMX6ULL's UART1 registers and pin multiplexing macros (example):
c
// UART1 register base address
#define UART1_BASE 0x02020000
#define UART1 ((volatile unsigned int *)UART1_BASE)
// Register offset definitions (only core registers listed)
#define UART_URXD 0x00 // Receive register
#define UART_UTXD 0x40 // Transmit register
#define UART_UCR1 0x80 // Control register 1
#define UART_UCR2 0x84 // Control register 2
#define UART_UCR3 0x88 // Control register 3
#define UART_UFCR 0x90 // FIFO control register
#define UART_USR2 0xB8 // Status register 2
#define UART_UBIR 0xA4 // Baud rate increment register
#define UART_UBMR 0xA8 // Baud rate modulation register
// Pin multiplexing function declarations (IMX6ULL standard library)
void IOMUXC_SetPinMux(unsigned int muxRegister, unsigned int muxMode);
void IOMUXC_SetPinConfig(unsigned int configRegister, unsigned int configValue);
2. Step 1: Clock Initialization
IMX6ULL's UART reference clock is provided by the peripheral clock by default. Here, we configure the base clock as 80MHz with a 1:1 prescaler (i.e., reference clock = 80MHz):
c
/**
* @brief UART clock initialization: 80MHz base clock, 1:1 prescaler
*/
void uart_clk_init(void) {
// Configure UART1's peripheral clock as 80MHz (specific registers refer to IMX6ULL clock tree)
CCM->CSCDR1 &= ~(0x1F << 6); // Clear existing prescaler value
CCM->CSCDR1 |= (0x00 << 6); // 1:1 prescaler, UART reference clock = 80MHz
}
3. Step 2: Pin Initialization
IMX6ULL pins are "multi-function multiplexed". Configure the specified pins as UART1_TX/RX and set electrical properties:
c
/**
* @brief UART1 pin initialization: TX->GPIO1_IO04, RX->GPIO1_IO05
*/
void uart_pin_init(void) {
// 1. Configure TX pin (GPIO1_IO04 → UART1_TX)
IOMUXC_SetPinMux(IOMUXC_UART1_TX_DATA_UART1_TX, 0); // Multiplex as UART1_TX
// Configure electrical properties: pull-up, 100MHz speed, drive strength, etc. (0x10B0 is standard)
IOMUXC_SetPinConfig(IOMUXC_UART1_TX_DATA_UART1_TX, 0x10B0);
// 2. Configure RX pin (GPIO1_IO05 → UART1_RX)
IOMUXC_SetPinMux(IOMUXC_UART1_RX_DATA_UART1_RX, 0); // Multiplex as UART1_RX
IOMUXC_SetPinConfig(IOMUXC_UART1_RX_DATA_UART1_RX, 0x10B0);
}
Parameter Explanation:
IOMUXC_SetPinMuxsecond parameter0: Use default multiplex mode with no additional configuration.0x10B0: Binary0001 0000 1011 0000, core configurations:- Bits [15:12]:
0001→ Pin speed 100MHz. - Bits [11:8]:
1011→ 22KΩ pull-up. - Bits [7:0]:
0000→ No open-drain, no interrupts, etc.
- Bits [15:12]:
4. Step 3: UART Register Configuration
Core configuration for UART operation mode (8 data bits, 1 stop bit, no parity, 115200 baud rate):
c
/**
* @brief UART1 register configuration: 8N1 (8 data bits + 1 stop bit + no parity), 115200 baud rate
*/
void uart_reg_init(void) {
// 1. Software reset UART (UCR2[0] set to 1, hold for 4 clock cycles, then clear to 0)
UART1[UART_UCR2/4] |= (1 << 0);
while((UART1[UART_UCR2/4] & (1 << 0))); // Wait for reset completion
// 2. Configure UCR1: Enable UART main switch (UCR1[0] = 1)
UART1[UART_UCR1/4] |= (1 << 0);
// 3. Configure UCR2: 8 data bits, 1 stop bit, no parity, enable transceiver, ignore RTS flow control
UART1[UART_UCR2/4] |= (1 << 1); // RXEN=1, enable receive
UART1[UART_UCR2/4] |= (1 << 2); // TXEN=1, enable transmit
UART1[UART_UCR2/4] &= ~(1 << 5); // WS=0, 8 data bits
UART1[UART_UCR2/4] &= ~(1 << 6); // STPB=0, 1 stop bit
UART1[UART_UCR2/4] &= ~(1 << 8); // PREN=0, disable parity
UART1[UART_UCR2/4] |= (1 << 14); // IRTS=1, ignore RTS flow control
// 4. Configure UCR3: RXDMUXSEL=1 (IMX6ULL must set this for multiplex mode)
UART1[UART_UCR3/4] |= (1 << 2);
// 5. Configure UFCR: RFDIV=0 (reference clock 1:1 prescaler)
UART1[UART_UFCR/4] &= ~(0x7 << 7); // Clear existing value
UART1[UART_UFCR/4] |= (0x0 << 7); // 1:1 prescaler
// 6. Configure baud rate: 115200 (reference clock 80MHz)
// Formula: BaudRate = Ref Freq/(16 * ((UBMR + 1)/(UBIR + 1)))
// Derivation: (UBMR+1)/(UBIR+1) = 80000000/(16*115200) ≈ 43.4028
// Choose UBIR=15, UBMR=659 ((659+1)/(15+1)=660/16=41.25, close to target)
UART1[UART_UBIR/4] = 15;
UART1[UART_UBMR/4] = 659;
}
5. Step 4: Implementation of Transmit/Receive Functions
Based on register encapsulation, implement putc (send a single character), puts (send a string), and getc (receive a single character):
c
/**
* @brief Send a single character
* @param d Character to send
*/
void putc(unsigned char d) {
// Poll TXDC bit (USR2[3]) until transmission is complete
while ((UART1[UART_USR2/4] & (1 << 3)) == 0);
UART1[UART_UTXD/4] = d; // Write to transmit register, hardware sends automatically
}
/**
* @brief Send a string (automatically appends newline)
* @param pStr Pointer to the string
*/
void puts(const char *pStr) {
while (*pStr) { // Traverse the string until '\0'
putc(*pStr++);
}
putc('\n'); // Append newline at the end
}
/**
* @brief Receive a single character (blocking)
* @return Received character
*/
unsigned char getc(void) {
// Poll RDR bit (USR2[0]) until data is ready
while ((UART1[UART_USR2/4] & (1 << 0)) == 0);
return (unsigned char)(UART1[UART_URXD/4] & 0xFF); // Read 8-bit data
}
Core Logic:
- Transmit : Check the
TXDCbit to ensure the previous character has been sent, avoiding data overwrite. - Receive : Check the
RDRbit to determine if new data is available, blocking until data is received.
6. Step 5: Porting the stdio Library (Supporting printf/scanf)
By default, bare-metal programs for IMX6ULL cannot use printf/scanf. The stdio library must be ported as follows:
(1) Add an Empty raise Function
Add the following function to uart.c (required by the stdio library; compilation will fail if missing):
c
// For stdio library compatibility; function body can remain empty
void raise(int n) {
(void)n; // Avoid unused parameter warning
}
(2) Modify the Assembly File Extension
Rename the startup file project/start.s to project/start.S:
.s: Direct compilation, no preprocessing..S: Preprocessing first (supports#include,#define, etc.), then compilation, adapting to macro definitions in bare-metal projects.
(3) Modify the Makefile
Add stdio library header/source paths and link the GCC library:
makefile
# Target file
target = imx6ull_uart
# Compiler paths (adjust based on your cross-compiler)
cc = arm-linux-gnueabihf-gcc
ld = arm-linux-gnueabihf-ld
objcopy = arm-linux-gnueabihf-objcopy
objdump = arm-linux-gnueabihf-objdump
# 1. Add stdio-related paths
incdirs = bsp imx6ull stdio/include # Header directories
srcdirs = bsp project stdio/lib # Source directories
# 2. Specify GCC library path (adjust based on your compiler path)
libpath = -lgcc -L/usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/lib/gcc/arm-linux-gnueabihf/4.9.4
# Compiler flags: Wall (enable warnings), thumb mode, no stdlib, disable built-ins
CFLAGS = -Wall -Wa,-mimplicit-it=thumb -nostdlib -fno-builtin
# Collect all source files
srcfiles = $(foreach dir, $(srcdirs), $(wildcard $(dir)/*.c $(dir)/*.S))
objfiles = $(patsubst %.c, %.o, $(patsubst %.S, %.o, $(srcfiles)))
# Link to generate ELF file
$(target).elf: $(objfiles)
$(ld) -Timx6ull.lds -o $@ $^ $(libpath)
# Compile C files
%.o: %.c
$(cc) $(CFLAGS) -I$(incdirs) -c -o $@ $<
# Compile assembly files
%.o: %.S
$(cc) $(CFLAGS) -I$(incdirs) -c -o $@ $<
# Generate BIN file
bin: $(target).elf
$(objcopy) -O binary $< $(target).bin
# Clean
clean:
rm -rf $(objfiles) $(target).elf $(target).bin
7. Test Code
Call the encapsulated functions in main.c to test UART communication:
c
#include "uart.h"
int main(void) {
unsigned char recv_data;
// Initialize UART
uart_clk_init();
uart_pin_init();
uart_reg_init();
// Send test string
puts("IMX6ULL UART Test Start!");
puts("Please input a char:");
while (1) {
recv_data = getc(); // Receive character
putc(recv_data); // Echo character
puts(" Recv Success!"); // Notify successful reception
}
return 0;
}
IV. Key Considerations
- Baud Rate Calculation: Must strictly match the reference clock to avoid garbled output.
- Pin Configuration: Refer to the manual for IMX6ULL UART pin multiplexing to avoid misconfiguration.
- Power Supply: Avoid powering the board via USB; use an external DC power supply to prevent overheating.
- Compilation: Ensure the compiler and library paths in the Makefile match your local environment.
V. Summary
This article provides a complete "concept → hardware → code" explanation of UART serial communication implementation for IMX6ULL. Key takeaways:
- Understand the fundamentals of UART asynchronous serial communication, including frame format and baud rate.
- Hardware-wise, focus on CH340's USB-to-TTL functionality and avoid incorrect power supply choices.
- Code-wise, the core lies in register configuration (especially baud rate and transmit/receive enable) and stdio library porting.
- Polling is the most basic bare-metal UART implementation; it can later be extended to interrupt/DMA modes.