一個嵌入式設備,串口基本上就是最常用到的外設了,通過串口可以將開發板和電腦連接,也有很多外設是通過串口來進行數據交互的。今天就來搞一下I.MX6UL的串口通訊,實現和電腦通訊的效果。
UART接口
I.MX6UL的串口外設叫做UART(Universal Asynchronous Receiver/Trasmitter),即異步串行收發器。UART作為串口的一種,其工作原理也是將數據位一幀一幀的進行傳輸,數據的發送和接收共用一條線纜,所以UART接口與外設相連的時候最少需要3根線:TXD、RXD和GND。下面的圖是UART的通訊格式
起始位(StartBit)是一個邏輯0
空閑位:在起始位前面的狀態為邏輯1,表示沒有數據,空閑中。
數據位:實際要傳輸的數據,一般是按照字節闡述,一個字節8位,低位在前先傳輸,高位在后面傳輸。
停止位(StopBit)邏輯1,位數可以是1、1.5或2個bit。
UART電平標准
UART一般接口電平有TTL和RS232電平,開發板上的TXD和RXD對應的就是TTL電平,使用低電平表示0,高電平表示1;而DB9接口就是對應的RS232接口,用-3~-15V表示邏輯1,+3~+15V表示邏輯0,采用差分線連接。在使用RS232時候一定要注意接口電平,不要燒毀外設。
由於現在電腦上基本都不帶COM口,而在寫單片機什么的需要串口,這就需要一個USB轉TTL電平的芯片。最常用的就是CH340。比如Arduino(nano版)的的背后就有個CH340C。用過這個芯片和USB連接就可以實現串口功能(很多USB轉232的設備就是用的這個芯片)。
I.MX6UL的UART接口
I.MX6UL提供了8組UART接口,結構體如下:
具備如下特點:
- 兼容TIA/EIA-232-F標准,最高速率5.0M/s
- 支持串行IR接口,兼容IrDA,最高速率115200bps
- 支持9位或多點的RS-485模式
- 232可以選擇7或8位的字符格式,485模式9bit格式
- 停止位1或2個bit
- 可編程的奇偶校驗
- 最高到115200bit/s自動波特率檢測
- 等等等等,太多了
IMX.6UL的UART的功能有非常多,我們這里只用做最基礎的串口通訊功能,具體的實際作用參考手冊Chapter 55給了非常詳細的介紹。
主要寄存器
UART相關寄存器也比較多,因為Soc一共有8組UART,這里截取;一組的寄存器映射
但是要注意的是,雖然8組UART里各個寄存器功能序列是一樣的,但是這個內存映射不是從UART1開始的,而是從UART7開始的。
下面看看幾個我們要用到的寄存器
UARTx_URXD
接收數據寄存器UART Receiver Register,寄存器結構如下:
寄存器全部為只讀,我們主要用到就是最后的低8位,用來存儲接收的數據。另外,bit[10]=1時可以在RS485模式下,數據結構為9bit時保存第九個bit的數據
UARTx_UTXD
發送數據寄存器UART Transmitter Register,用來存放待發送的數據。
寄存器低8位有效,在7bit數據結構下,bit[7]可以忽略,如果想要將數據寫入該寄存器,需要確認TRDY(UARTx_UCR1[13])必須為高電平,即當前沒有數據被發送。
UARTx_UCR1
控制寄存器1(UART Control Register 1)UART提供了4組控制寄存器用來對其進行功能設置,首先是UCR1,先看寄存器結構
ADEN(bit[15])Automatic Baud Rate Detection Interrupt Enable,自動波特率偵測中斷使能 ,允許ADET標志位(UARTx_USR2 bit[15])觸發中斷
ADBR(bit[14])Automatic Detection of Baud Rate,自動檢測波特率使能,大概意思就是當該位值為1且ADET被清除時,接收器通過接收一個字符A或者a,對比其ASCII碼為0x41或0x61,去確認合適的比特率。
TRDYEN(bit[13])Transmitter Ready Interrupt Enable,數據發送准備中斷使能
IDEN(bit[12])Idle Condition Detected Interrupt Enable,一個什么中斷使能,這個暫時沒搞懂,暫時應該也用不上
中間幾個中斷使能就不說了,最后一個就是UART的使能UARTEN(bit[0]),整個寄存器我們暫時應該也就是能用到這一個bit。(自動獲取波特率只能到115200,先關閉不用)
UARTx_UCR2
控制寄存器2,寄存器結構如下
手冊上有很詳細的解釋,這里只說一下需要用到的幾個
IRTS(bit[14])Ignore RTS Pin,1時忽略RTS引腳,我們在使用TTL電平串口信號時只用到RXD和TXD,RTS和CTS一般是不使用的,設置為1即可。
PREN(bit[8])Parity Enable,校驗使能,1時使能校驗功能
PROE(bit[7])Parity Odd/Even,校驗方式:1為奇校驗,0為偶校驗
STPB(bit[6])停止位,0時停止位1bit,1時2bit
WS(bit[5])Word Size,數據位長度,0時7bit,1時8bit(該長度不包含起始、結束及校驗位)
TXEN(bit[2])Transmitter Enable,發送數據使能,1時使能
RXEN(bit[1])Receiver Enable,接收數據使能
SRET(bit[0])Software Reset,軟件復位,寫0時對FIFO,USR1,USR2,UBIR,UBMR,UBRC,URXD,UTXD和UTS[6:3]進行復位,但復位前保留4個時鍾周期用來進行其他的操作。復位后該位自動置1。
UARTx_UCR3
控制寄存器3
這個我們只用到了一個RXDMUXSEL,因為手冊上說了這個應給被置1
其他的位我們暫時也都用不到。
UARTx_UFCR
緩存控制寄存器UART FIFO Control Register,這里我們主要用來設置分頻器
RFDIV(bit[9:7])里定義了從CCM過來的時鍾的分頻
注意這個分頻不是按照數值+1的模式進行分頻的,看具體的值,這個分頻器決定的UART的參考時鍾
UARTx_USR2
狀態寄存器1我們也用不到,這里要用到狀態寄存器2
ADET(bit[15])Automatic Baud Rate Detect Complete,波特率檢測完畢,當1時接收到合適的A或者a字符,需要寫1清除狀態
TXFE(bit[14])Transmit Buffer FIFO Empty,發送緩存狀態,1時表示緩存區為空
TXDC(bit[3])Transmitter Complete,發送完成標志位,1時表示發送數據完成,發送寄存器或發送緩存寫入數據,該位自動清零
RDR(bit[0])Receive Data Ready,數據接收標志位,為1時表示至少還有1個數據要接收
UARTx_UBIR和UARTx_UBMR
用來湊波特率的兩個寄存器,參考手冊第55.5章節介紹了波特率的計算方法
RefFreq就是經過分頻后的參考時鍾,比如我們時鍾為80MHz,分頻為1分頻,想要用115200的波特率,就要自己湊了,正點原子給出的數據是UBMR=3124,UBIR=71,那么
其實NGP給了個函數,可以根據我們需要的波特率計算出對應的參數。
UART使用
使用UART的流程和其他的外設差不多也是先初始化、再使用
時鍾源設置
有一點要注意:修改時鍾樹對應的時鍾源,UART和其他的外設用到的不是一個時鍾源,我們前面的用到的都是IPG_CLK,UART用到的的是UART_CLK_ROOT
我們需要通過CSCDR1選擇6分頻的pll3(480MHz),也就是80MHz,后面分頻器為1分頻。根據手冊可以查出,UART_CLK_SEL為bit[6],值應為1,分頻器UART_CLK_PODR對應bit[5:0],對應2^6+1分頻,1分頻值為0。
所以要修改我們的clk初始化函數clk_init
/*--------------------------UART_CLK設置--------------------------*/ /*UART_CLK_ROOT主頻設置為80MHz*/ CCM->CSCDR1 &= ~(1<<6); //CSCDR1[UART_CLK_SEL](bit[6])=0,時鍾源80MHz CCM->CSCDR1 &= ~(7<<0); //CSCDR1[UART_CLK_PODF](bit[5:0])設置為0,對應1分頻 /*-------------------------UART_CLK設置完畢------------------------*/
這步一定要記得!否則波特率就亂了!我在調試的時候就是忘了這一步!
UART初始化
UART的初始化包括IO的復用設置、UART參數設置、波特率設置。主要就是設置UCR1、UCR2、UCR3、UFCR、UBIR、UBMR幾個寄存器。在設置寄存器值時,應該按照下面的順序
- 關閉串口功能(UARTEN=0)
- 復位UART(SRET=0),復位時等待SRET為1,即復位完畢
- 設置相關寄存器的值
- 使能UART
配置寄存器的過程如下:
/*配置UART1*/ UART1->UCR1 = 0; // UART1->UCR1 &= ~(1<<14); /*配置UCR2*/ UART1->UCR2 = 0; //清除UCR0 UART1->UCR2 |= (1<<1) |(1<<2) |(1<<5)|(1<<14); //從左起:RXEN=1 TXEN=1 WS=1 IRTS=1 //接收、發送使能、數據長度為8bit 忽略RTS引腳 /*配置UCR3*/ UART1->UCR3 |= (1<<2); //RXDMUXSEL=1 //波特率設置115200 UART1->UFCR &= ~(7<<7); //RFDIV進行清零 UART1->UFCR = 5<<7; //設置1分頻,uart_clk=80MHz UART1->UBIR = 71; UART1->UBMR = 3124;
其實還是比較簡單的。
其他的幾個關閉、使能等函數放在最后。
數據接收、發送
數據的發送、接收就是對URXD、UTXD的低8位進行操作
/** * @brief 通過UART1發送1個字符 * * @param c 待發送的字符 */ void putc(unsigned char c) { while(((UART1->USR2 >>3) & 0x01) == 0); //等待前一個發送流程完畢 UART1->UTXD = (c & 0xFF); } /*通過UART1接收一個字符*/ unsigned char getc(void) { while(((UART1->USR2)&0x01) == 0); //等待前一個接收流程完畢 return UART1->URXD; } /** * @brief 發送字符串 * * @param str 待發送的字符串 */ void puts(unsigned *str) { char *p = str; while(*p){ putc(*p++); } }
這樣就完成了所有的功能定義。
文件結構:
UART功能的文件結構和其他外設一樣
兩個文件如下:

/** * @file bsp_uart.c * @author your name (you@domain.com) * @brief uart功能定義 * @version 0.1 * @date 2022-01-17 * * @copyright Copyright (c) 2022 * */ #include "bsp_uart.h" //初始化uart1,波特率固定為115200 void uart_init(void) { uart_io_init(); //IO初始化 uart_disable(UART1); //關閉串口 uart_softreset(UART1); //復位UART1 /*配置UART1*/ UART1->UCR1 = 0; // UART1->UCR1 &= ~(1<<14); /*配置UCR2*/ UART1->UCR2 = 0; //清除UCR0 UART1->UCR2 |= (1<<1) |(1<<2) |(1<<5)|(1<<14); //從左起:RXEN=1 TXEN=1 WS=1 IRTS=1 //接收、發送使能、數據長度為8bit 忽略RTS引腳 /*配置UCR3*/ UART1->UCR3 |= (1<<2); //RXDMUXSEL=1 //波特率設置115200 UART1->UFCR &= ~(7<<7); //RFDIV進行清零 UART1->UFCR = 5<<7; //設置1分頻,uart_clk=80MHz UART1->UBIR = 71; UART1->UBMR = 3124; uart_enable(UART1); //使能UART1 } /** * @brief IO初始化為UART * */ void uart_io_init(void) { IOMUXC_SetPinMux(IOMUXC_UART1_TX_DATA_UART1_TX,0);//復用為UART1_TX IOMUXC_SetPinConfig(IOMUXC_UART1_TX_DATA_UART1_TX,0x10b0); IOMUXC_SetPinMux(IOMUXC_UART1_RX_DATA_UART1_RX,0); IOMUXC_SetPinConfig(IOMUXC_UART1_RX_DATA_UART1_RX,0x10b0); } /** * @brief 關閉UART串口 * * @param base UART結構體 */ void uart_disable(UART_Type *base) { base->UCR1 &= (1<<0); } /** * @brief 使能UART串口 * * @param base UART結構體 */ void uart_enable(UART_Type *base) { base->UCR1 |= (1<<0); } /** * @brief UART軟復位 * * @param base UART結構體 */ void uart_softreset(UART_Type *base) { base->UCR2 &= ~(1<<0); //SRET=0 while((base->UCR2 & 0x01) == 0 ); //復位完畢,SRET=1 } /** * @brief 通過UART1發送1個字符 * * @param c 待發送的字符 */ void putc(unsigned char c) { while(((UART1->USR2 >>3) & 0x01) == 0); //等待前一個發送流程完畢 UART1->UTXD = (c & 0xFF); } /*通過UART1接收一個字符*/ unsigned char getc(void) { while(((UART1->USR2)&0x01) == 0); //等待前一個接收流程完畢 return UART1->URXD; } /** * @brief 發送字符串 * * @param str 待發送的字符串 */ void puts(unsigned *str) { char *p = str; while(*p){ putc(*p++); } }
頭文件

/** * @file bsp_uart.h * @author your name (you@domain.com) * @brief uart頭文件 * @version 0.1 * @date 2022-01-17 * * @copyright Copyright (c) 2022 * */ #ifndef __BSP_UART_H #define __BSP_UART_H #include "imx6ul.h" void uart_io_init(void); void uart_disable(UART_Type *base); void uart_enable(UART_Type *base); void uart_softreset(UART_Type *base); void putc(unsigned char c); unsigned char getc(void); void puts(unsigned *str); #endif
在main函數里導入頭文件以后,調用函數
int_init(); imx6u_clkinit(); clk_enable(); uart_init(); while(1) { puts("input a char"); a=getc(); putc(a); puts("\r\n"); puts("your input is:"); putc(a); puts("\r\n"); }
就可以使用串口實現數據交互了。
PC上運行SecureCRT,使用串口連接,Soc從PC串口接收一個字符,然后返回給PC,就是這么個效果。
波特率計算
前面我們已經實現了數據的通訊,但是波特率是固定在115200,並且波特率的計算也是我們湊出來了,可以如果我們需要9600的比特率,還要在湊半天。NXP給我們的SDK包里提供了一個函數,可以直接設置對應的寄存器,這個函數可以直接調用
/** * @brief 設置比特率(官方代碼) * * @param base UART結構特 * @param baudrate 要設置的比特率 * @param srcclock_hz */ void uart_setbaudrate(UART_Type *base, unsigned int baudrate, unsigned int srcclock_hz) { uint32_t numerator = 0u; //分子 uint32_t denominator = 0U; //分母 uint32_t divisor = 0U; uint32_t refFreqDiv = 0U; uint32_t divider = 1U; uint64_t baudDiff = 0U; uint64_t tempNumerator = 0U; uint32_t tempDenominator = 0u; /* get the approximately maximum divisor */ numerator = srcclock_hz; denominator = baudrate << 4; divisor = 1; while (denominator != 0) { divisor = denominator; denominator = numerator % denominator; numerator = divisor; } numerator = srcclock_hz / divisor; denominator = (baudrate << 4) / divisor; /* numerator ranges from 1 ~ 7 * 64k */ /* denominator ranges from 1 ~ 64k */ if ((numerator > (UART_UBIR_INC_MASK * 7)) || (denominator > UART_UBIR_INC_MASK)) { uint32_t m = (numerator - 1) / (UART_UBIR_INC_MASK * 7) + 1; uint32_t n = (denominator - 1) / UART_UBIR_INC_MASK + 1; uint32_t max = m > n ? m : n; numerator /= max; denominator /= max; if (0 == numerator) { numerator = 1; } if (0 == denominator) { denominator = 1; } } divider = (numerator - 1) / UART_UBIR_INC_MASK + 1; switch (divider) { case 1: refFreqDiv = 0x05; break; case 2: refFreqDiv = 0x04; break; case 3: refFreqDiv = 0x03; break; case 4: refFreqDiv = 0x02; break; case 5: refFreqDiv = 0x01; break; case 6: refFreqDiv = 0x00; break; case 7: refFreqDiv = 0x06; break; default: refFreqDiv = 0x05; break; } /* Compare the difference between baudRate_Bps and calculated baud rate. * Baud Rate = Ref Freq / (16 * (UBMR + 1)/(UBIR+1)). * baudDiff = (srcClock_Hz/divider)/( 16 * ((numerator / divider)/ denominator). */ tempNumerator = srcclock_hz; tempDenominator = (numerator << 4); divisor = 1; /* get the approximately maximum divisor */ while (tempDenominator != 0) { divisor = tempDenominator; tempDenominator = tempNumerator % tempDenominator; tempNumerator = divisor; } tempNumerator = srcclock_hz / divisor; tempDenominator = (numerator << 4) / divisor; baudDiff = (tempNumerator * denominator) / tempDenominator; baudDiff = (baudDiff >= baudrate) ? (baudDiff - baudrate) : (baudrate - baudDiff); if (baudDiff < (baudrate / 100) * 3) { base->UFCR &= ~UART_UFCR_RFDIV_MASK; base->UFCR |= UART_UFCR_RFDIV(refFreqDiv); base->UBIR = UART_UBIR_INC(denominator - 1); //要先寫UBIR寄存器,然后在寫UBMR寄存器,3592頁 base->UBMR = UART_UBMR_MOD(numerator / divider - 1); } }
make的事項
在導入上面自動設置波特率的函數以后,在make的時候會報錯
錯誤提示是變量未定義,原因是我們調用uart_setbaudrate這個函數時候需要進行除法運算,而ARM沒有除法運算的硬件結構,進行除法運算需要借助軟件編譯器,軟浮點的實現是在一個叫做libgcc.a的庫中。這個庫需要我們在編譯的時候指定。因為我直接用到樹莓派自帶的交叉編譯器,庫的地址可以在/lib路徑下搜一下:
教程用的交叉編譯器版本是4.9.4,我用的是8.3
暫時還沒出現什么問題, 記錄下libgcc.a的路徑,添加在makefile中
1 CC := $(CROSS_COMPILE)gcc 2 LD := $(CROSS_COMPILE)ld 3 OBJCOPY := $(CROSS_COMPILE)objcopy 4 OBJDUMP := $(CROSS_COMPILE)objdump 5 6 LIBPATH := -lgcc -L /lib/gcc/arm-linux-gnueabihf/8 #制定依賴庫路徑 7 8 $(TARGET).bin : $(OBJS) 9 10 $(LD) -Timx6ul.lds -o $(TARGET).elf $^ $(LIBPATH) #將所有依賴文件鏈接,生成.elf文件 11 $(OBJCOPY) -O binary -S $(TARGET).elf $@ #將elf轉換為依賴的目標集合 12 $(OBJDUMP) -D -m arm $(TARGET).elf > $(TARGET).dis #將elf文件反匯編
要對原先的通用Makefile進行修改:
- 添加第6行,通過一個變量指定依賴的路徑
- 修改第4行,在鏈接的時候引用變量
修改完了make一下,看到會報一個錯!
原因是我們定義的putc和puts兩個函數和libgcc.a庫里的原生的函數重名了。要解決這個問題還是修改Makefile文件
1 # 靜態模式 <Targets...>:<tatgets-pattern>:<prereq-patterns...>下面兩天為自寫 2 $(SOBJS) : obj/%.o : %.s #將所有的.s文件編譯成.o文件放在obj文件夾內 3 $(CC) -Wall -nostdlib -fno-builtin -c -O2 $(INCLUDE) -o $@ $< 4 5 $(COBJS) : obj/%.o : %.c 6 $(CC) -Wall -nostdlib -fno-builtin -c -O2 $(INCLUDE) -o $@ $<
在第3、6行加上參數-fno-builtin,意思是不調用C語言的內建函數。這樣調用的函數就是我們自己定義的函數了。
make以后還是有個錯誤
通過提示大概意思就是要定義一個異常處理的函數raise,對應的idiv0我覺得意思是當除數為0時候的異常處理。我們定義一個空函數就可以了。
void raise(int sig_nr) { }
在頭文件里聲明,搞定!
printf格式化函數的移植
我們前面的串口驅動,只能發送一般的字符,如果需要輸出數字的時候還要將數字轉換為字符,很不方便。一般很常用的方法就是把printf函數映射到串口上,那樣就可以直接使用printf函數來完成格式化輸出了。
庫移植
將教程提供的stdio文件夾復制到項目根目錄下,修改Makefile文件
從文件名稱就可以看出來,目錄下include文件夾里的是頭文件,lib里是源代碼,將該路徑添加到Makefile里。進行make。
函數調用
導入這個庫以后就可以直接使用格式化輸入和輸出了
int a,b; while(1) { printf("請輸入兩個值,用空格隔開"); scanf("%d %d",&a,&b); printf("\r\n 數據%d+%d=%d\r\n",a,b,a+b); }
上面的代碼是在main函數中的,前面初始化串口、時鍾什么的我沒有截取,主要就是看一下怎么使用兩個函數。但是要注意一點:被移植的printf不支持浮點類運算!!!
這里跟教程有些區別:
前面說過,正點原子提供的教程上使用的交叉編譯器什4.9.4,而我用到時8.3,我對照在X86架構下使用4.9.4在make的時候會報錯:
錯誤信息thumb conditional instruction should be in IT block -- `addcs r5,r5,#65536',這個指令集錯誤我沒有找到出處,解決辦法是在編譯C文件時候加上一個參數:Wa,-mimplicit-it=thumb(百度上直接給的方案,沒有找到具體的解決流程和原因)
修改后的Makefile
# 靜態模式 <Targets...>:<tatgets-pattern>:<prereq-patterns...>下面兩天為自寫 $(SOBJS) : obj/%.o : %.s #將所有的.s文件編譯成.o文件放在obj文件夾內 $(CC) -Wall -nostdlib -fno-builtin -c -O2 $(INCLUDE) -o $@ $< $(COBJS) : obj/%.o : %.c $(CC) -Wall -Wa,-mimplicit-it=thumb -nostdlib -fno-builtin -c -O2 $(INCLUDE) -o $@ $<
燒錄sd卡就行了。
調試時候的BUG
在最后調試的時候出現了一個大BUG,這里記錄一下吧,免得以后忘了!
開始怎么也沒搞清,現象就是添加玩stdio庫以后make能成功,但是用imxdownload下載一直報錯
先后試過教程提供的源碼,更換了交叉編譯器,重新編譯了下載軟件一直都不行,按理說報dd錯誤是磁盤寫入失敗,燒錄前面的所有程序都可以,在后來發現只要寫入到文件size沒有超過10000Bytes都正常,就沒想過是卡的問題。直到發現燒錄完以前的程序發現上電后初始化非常慢,想到卡可能出問題了,用fdisk格式化失敗,Ubuntu下使用好幾個磁盤工具格式化都報錯,換了個讀卡器也不行。沒辦法找了個win10的PC,格式化了一下,還是不行,用Imager燒錄了個樹莓派的鏡像,沒問題,回來重新燒錄一遍,竟然好了!估計是最近經常用讀卡器是不是有什么問題了。