今天学习ARM裸机下UART串口。串口是嵌入式开发最基础的调试工具,打印日志、收发指令全都靠它。以前直接用printf,没关心底层怎么实现;今天搞懂串行通信基础概念,手写UART寄存器配置,还把stdio库移植到裸机,直接能用scanf、printf,收获挺大。
一、串口通信基础概念
嵌入式通信分为并行通信和串行通信。
并行通信同一时刻传输多个比特,速度快,但是占用引脚多;串行通信把数据拆成一比特一比特,在一根线上依次传输,引脚占用少,硬件简单,UART串口就属于串行里面的异步通信。
按照数据传输方向分为三种模式:
- 单工:只能单向传输,一端固定发送、一端固定接收;
- 半双工:双向可以通信,但同一时刻只能一方发、另一方收,不能同时收发,类似对讲机;
- 全双工:两根数据线,收发独立,可以同时发送和接收,就像打电话,我们常用的TTL‑UART、RS232都是全双工;RS485是差分信号,属于半双工。
同步通信 、异步通信
同步通信:除数据线之外额外带一根时钟线,收发双方跟着同一个时钟节拍采样数据,例如I2C、SPI;
异步通信(UART):没有时钟线,收发双方事先约定波特率,依靠起始位、停止位来区分字节,双方本地时钟误差不能超过6%。
UART一帧数据格式(TTL电平):
空闲状态:数据线高电平;
起始位:拉低1个bit时间,代表一帧开始;
数据位:从最低位LSB开始发送;
校验位:无校验、奇校验、偶校验;
停止位:拉高,标记本字节结束。
波特率bps:每秒传输的比特数,常用:9600、115200。波特率决定每一位电平持续时间。
电气标准区别:
TTL:芯片引脚电平,3.3V/5V,传输距离很短;我们开发板CH340芯片做USB转TTL,电脑USB转成TTL电平给到IMX6ULL的UART引脚;
RS232:使用正负电压,逻辑1为‑3V‑15V,逻辑0为+3V+15V,距离最远二三十米;
RS485:差分信号A/B,依靠电压差值识别信号,抗干扰强,可以传到1200米。
二、IMX6ULL UART硬件与寄存器要点
我们板子上CH340负责USB转TTL,连接到UART1的TX、RX引脚。写裸机UART,步骤固定:
- 开启UART外设时钟(时钟系统前面学过,CCGR时钟门控)
- IO引脚复用配置:把普通GPIO复用为UART_TX、UART_RX,同时配置电气属性
- UART软件复位,配置控制寄存器UCR1、UCR2、UCR3
- 配置分频寄存器UFCR,设置波特率相关UBIR、UBMR
- 使能发送、接收,打开UART模块总开关
重点寄存器:
UTXD:发送寄存器,写入数据就启动发送; URXD:接收寄存器,只读,存放收到字节;
UCR1:bit0 UARTEN,UART模块总开关; UCR2:软件复位位、RX接收使能、TX发送使能、数据位长度、停止位、校验位;
UCR3:UART多路复用模式,IMX6ULL必须把bit2置1; UFCR:参考时钟分频;
UBIR / UBMR:波特率生成,波特率计算公式: BaudRate = Ref Freq/(16 \times ((UBMR + 1) / (UBIR + 1))) USR2状态寄存器:bit3 TXDC发送完成标志;bit0 RDR接收数据就绪标志。
三、UART1底层驱动代码
c
#include "uart.h"
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
void uart1_init(void)
{
//1.引脚复用:配置TX RX为UART功能
IOMUXC_SetPinMux(IOMUXC_UART1_TX_DATA_UART1_TX, 0);
IOMUXC_SetPinMux(IOMUXC_UART1_RX_DATA_UART1_RX, 0);
//2.配置引脚电气属性
IOMUXC_SetPinConfig(IOMUXC_UART1_TX_DATA_UART1_TX, 0x10B0);
IOMUXC_SetPinConfig(IOMUXC_UART1_RX_DATA_UART1_RX, 0x10B0);
//UART软件复位
UART1->UCR2 &= ~(1 << 0);
unsigned int r = 0;
r = UART1->UCR2;
r |= (1 << 14); //忽略RTS流控
r &= ~(1 << 8); //关闭奇偶校验
r &= ~(1 << 6); //1位停止位
r |= (1 << 5); //8位数据位
r |= (1 << 2); //TX发送使能
r |= (1 << 1); //RX接收使能
UART1->UCR2 = r;
//IMX6ULL UART必须设置RXDMUXSEL=1
UART1->UCR3 |= (1 << 2);
//设置参考时钟分频
UART1->UFCR &= ~(7 << 7);
UART1->UFCR |= (5 << 7);
//波特率参数配置
UART1->UBIR = 999;
UART1->UBMR = 43402;
//打开UART模块总使能
UART1->UCR1 |= (1 << 0);
}
//发送单个字符
void putc(unsigned char c)
{
//等待发送完成标志TXDC
while((UART1->USR2 & (1 << 3)) == 0);
UART1->UTXD = c;
}
//发送字符串
void puts(const char *s)
{
while (*s)
{
putc(*s++);
}
putc('\n');
}
//接收单个字符,阻塞等待
unsigned char getc(void)
{
//等待接收数据就绪RDR
while((UART1->USR2 & (1 << 0)) == 0);
return UART1->URXD & 0xFF;
}
//移植stdio库必须提供空的raise函数,否则编译报错
void raise(int n){
}
裸机移植C标准库stdio要注意:
- 必须实现空函数
void raise(int n); - 启动汇编文件后缀要用大写
start.S,大写S会做预处理;小写s不会预处理; - Makefile需要增加stdio头文件路径、源码路径,链接时加入libgcc库。
底层写好putc、getc之后,标准库的printf、scanf内部就会调用这两个底层函数,于是裸机就可以直接使用格式化输入输出。
四、main主函数测试代码
这里复用我们之前写好的时钟、GPT定时器驱动,初始化顺序:中断控制器 → 时钟初始化 → 外设初始化,最后初始化uart1。
测试功能:串口输入两个整数,板子计算加法,把结果打印回串口工具。
c
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "led.h"
#include "beep.h"
#include "key.h"
#include "interrupt.h"
#include "clock.h"
#include "epit.h"
#include "gpt.h"
#include "uart.h"
#include "stdio.h"
int main(void)
{
system_interrupt_init();
clock_init();
led_init();
beep_init();
key_init();
//epit1_init();
gpt1_init();
uart1_init();
unsigned int num1 = 0, num2 = 0;
while(1)
{
led_nor();
//串口输入两个数字,板子做加法运算回显
scanf("%d %d", &num1, &num2);
printf("%d + %d = %d\n",num1, num2, num1 + num2);
}
return 0;
}
现象:打开串口调试助手,波特率和代码配置保持一致,输入两个数字按回车,开发板计算加法并打印算式结果。也可以把注释打开,直接调用getc()接收单字符,puts()输出字符串。
小结
- UART是异步串行通信,没有时钟线,全靠双方约定好波特率,依靠起始、停止位识别每一帧数据;和I2C/SPI这类同步通信要区分开。TTL、RS232、RS485是不同电气物理标准,逻辑电平、传输距离各不相同。
- IMX6ULL裸机UART开发流程:时钟 → IO复用配置 → 寄存器复位配置 → 波特率配置 → 使能收发模块,任何一步出错串口都无法正常收发。特别记住UCR3的bit2必须置1,这是IMX6ULL的坑点。
putc、getc是最底层字符收发,实现这两个接口之后,就可以移植stdio,直接使用printf、scanf格式化输入输出,极大方便裸机调试。