一個嵌入式設備,串口基本上就是最常用到的外設了,通過串口可以將開發板和電腦連接,也有很多外設是通過串口來進行數據交互的。今天就來搞一下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接口,結構體如下:
具備如下特點:
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幾個寄存器。在設置寄存器值時,應該按照下面的順序
配置寄存器的過程如下:
/*配置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"http://初始化uart1,波特率固定為115200void 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++); }}bsp_uart.c
頭文件
/** * @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);#endifbsp_uart.h
在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 pisor = 0U; uint32_t refFreqDiv = 0U; uint32_t pider = 1U; uint64_t baudDiff = 0U; uint64_t tempNumerator = 0U; uint32_t tempDenominator = 0u; /* get the approximately maximum pisor */ numerator = srcclock_hz; denominator = baudrate << 4; pisor = 1; while (denominator != 0) { pisor = denominator; denominator = numerator % denominator; numerator = pisor; } numerator = srcclock_hz / pisor; denominator = (baudrate << 4) / pisor; /* 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; } } pider = (numerator - 1) / UART_UBIR_INC_MASK + 1; switch (pider) { 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/pider)/( 16 * ((numerator / pider)/ denominator). */ tempNumerator = srcclock_hz; tempDenominator = (numerator << 4); pisor = 1; /* get the approximately maximum pisor */ while (tempDenominator != 0) { pisor = tempDenominator; tempDenominator = tempNumerator % tempDenominator; tempNumerator = pisor; } tempNumerator = srcclock_hz / pisor; tempDenominator = (numerator << 4) / pisor; 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 / pider - 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進行修改:
修改完了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,對應的ip0我覺得意思是當除數為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燒錄了個樹莓派的鏡像,沒問題,回來重新燒錄一遍,竟然好了!估計是最近經常用讀卡器是不是有什么問題了。
本文由 貴州做網站公司 整理發布,部分圖文來源于互聯網,如有侵權,請聯系我們刪除,謝謝!
網絡推廣與網站優化公司(網絡優化與推廣專家)作為數字營銷領域的核心服務提供方,其價值在于通過技術手段與策略規劃幫助企業提升線上曝光度、用戶轉化率及品牌影響力。這...
在當今數字化時代,公司網站已成為企業展示形象、傳遞信息和開展業務的重要平臺。然而,對于許多公司來說,網站建設的價格是一個關鍵考量因素。本文將圍繞“公司網站建設價...
在當今的數字化時代,企業網站已成為企業展示形象、吸引客戶和開展業務的重要平臺。然而,對于許多中小企業來說,高昂的網站建設費用可能會成為其發展的瓶頸。幸運的是,隨...
騰訊QQ如何下載?在軟件商店或是到官網就可以下載,在桌面建一個快捷就可以了。1、再打開網絡瀏覽器,在搜索引擎內然后輸入(直接下載)點擊搜索。2、你選一個網站點擊(立即上網下載)。3、在等待去下載結束后直接點擊(運行)。4、點擊(立即完全安裝)耐心的等待安裝完成即可建議使用電腦軟件啦。拓展:手機和電腦互傳文件,首先前提是要不滿足看看兩個條件:1.手機安卓版本一般不低于4.12,電腦前提是為或以上。2...
手機撥號上網是什么意思?撥號設置是設置默認使用哪張手機卡打電話,上網是開通數據,使用默認使用手機卡的流量。手機如何撥號上網?先打開撥號選項;點擊手機的撥號盤,點擊右下角三個點的設置選項;點擊【設置】,部分手機點擊【更多】選項按鈕顯示【設置】;可以看到,除了1是語音郵件設置,還可以設置2到9的快速撥號鍵。點擊添加聯系人,根據自己的實際情況在通訊錄中選擇經常聯系的聯系人。按照彈出的提示一步一步操作。1...
淘寶的特價是怎樣設置出來的?1、先打開淘寶首頁,直接點擊右上角“賣家中心”,輸入賬號密碼,剛剛進入商家后臺;2、左側找到“店鋪營銷工具”,點擊進入,找不到“單品寶”,再點擊進入;3、再點擊“剛建活動”;4、按步驟如何填寫活動名稱、活動時間等(注:活動時間最長可設置中180天),如需折扣商品只有那一個價格即選擇“商品級”,如有多個完全不同價格即選擇類型“SKU級”;5、選擇類型要折扣的活動商品,直接...