今天学习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状态寄存器:bit3TXDC发送完成标志;bit0RDR接收数据就绪标志。
三、UART1底层驱动代码
#include"uart.h"#include"MCIMX6Y2.h"#include"fsl_iomuxc.h"voiduart1_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);unsignedintr=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=1UART1->UCR3|=(1<<2);//设置参考时钟分频UART1->UFCR&=~(7<<7);UART1->UFCR|=(5<<7);//波特率参数配置UART1->UBIR=999;UART1->UBMR=43402;//打开UART模块总使能UART1->UCR1|=(1<<0);}//发送单个字符voidputc(unsignedcharc){//等待发送完成标志TXDCwhile((UART1->USR2&(1<<3))==0);UART1->UTXD=c;}//发送字符串voidputs(constchar*s){while(*s){putc(*s++);}putc('\n');}//接收单个字符,阻塞等待unsignedchargetc(void){//等待接收数据就绪RDRwhile((UART1->USR2&(1<<0))==0);returnUART1->URXD&0xFF;}//移植stdio库必须提供空的raise函数,否则编译报错voidraise(intn){}裸机移植C标准库stdio要注意:
- 必须实现空函数
void raise(int n); - 启动汇编文件后缀要用大写
start.S,大写S会做预处理;小写s不会预处理; - Makefile需要增加stdio头文件路径、源码路径,链接时加入libgcc库。
底层写好putc、getc之后,标准库的printf、scanf内部就会调用这两个底层函数,于是裸机就可以直接使用格式化输入输出。
四、main主函数测试代码
这里复用我们之前写好的时钟、GPT定时器驱动,初始化顺序:中断控制器 → 时钟初始化 → 外设初始化,最后初始化uart1。
测试功能:串口输入两个整数,板子计算加法,把结果打印回串口工具。
#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"intmain(void){system_interrupt_init();clock_init();led_init();beep_init();key_init();//epit1_init();gpt1_init();uart1_init();unsignedintnum1=0,num2=0;while(1){led_nor();//串口输入两个数字,板子做加法运算回显scanf("%d %d",&num1,&num2);printf("%d + %d = %d\n",num1,num2,num1+num2);}return0;}现象:打开串口调试助手,波特率和代码配置保持一致,输入两个数字按回车,开发板计算加法并打印算式结果。也可以把注释打开,直接调用getc()接收单字符,puts()输出字符串。
小结
- UART是异步串行通信,没有时钟线,全靠双方约定好波特率,依靠起始、停止位识别每一帧数据;和I2C/SPI这类同步通信要区分开。TTL、RS232、RS485是不同电气物理标准,逻辑电平、传输距离各不相同。
- IMX6ULL裸机UART开发流程:时钟 → IO复用配置 → 寄存器复位配置 → 波特率配置 → 使能收发模块,任何一步出错串口都无法正常收发。特别记住UCR3的bit2必须置1,这是IMX6ULL的坑点。
putc、getc是最底层字符收发,实现这两个接口之后,就可以移植stdio,直接使用printf、scanf格式化输入输出,极大方便裸机调试。