C與ARM彙編結合實現mini2440串口uart簡單程式
- 2020 年 3 月 6 日
- 筆記
最近學完了ARM的一些基礎知識,開始在mini2440上開發一些簡單的程式,串口發送程式是一開始涉及多個暫存器的例子,稍有繁多的步驟應該是開發過程中要慢慢適應的境況
下面的程式的目的是實現mini2440串口的發送功能,向超級終端列印簡單字元。
設備:mini2440如圖,軟體為gcc交叉編譯工具,minitools與超級終端,主機環境為Windows虛擬機WSL(版本為ubuntu18.04)

首先應該為C語言主程式的運行初始化環境(設定好堆、棧、入口、中斷向量表),這部分要用彙編實現,下面是這一部分
start.s:
.text .global _start _start: ldr pc, _reset ldr pc, _undefined_instruction ldr pc, _software_interrupt ldr pc, _prefetch_abort ldr pc, _data_abort nop ldr pc, _irq ldr pc, _fiq _reset: .word reset _undefined_instruction: .word undefined_instruction _software_interrupt: .word software_interrupt _prefetch_abort: .word prefetch_abort _data_abort: .word data_abort _irq: .word irq _fiq: .word fiq reset: @先設置向量表的位置為0x30001000 ldr r0,= 0x30001000 mcr p15, 0, r0, c12, c0, 0 init_stack: @初始化棧 ldr r0,= sk mov sp, r0 //svc sub r0, #512 msr cpsr, #0xd2 mov sp, r0 //irq sub r0, #512 msr cpsr, #0xd1 mov sp, r0 //fiq sub r0, #0 msr cpsr, #0xd7 mov sp, r0 //abort sub r0, #0 msr cpsr, #0xdb mov sp, r0 //undefine sub r0, #0 msr cpsr, #0x10 mov sp, r0 //user b main b main_end undefined_instruction: software_interrupt: stmfd sp!, {r1 - r3, lr} ldmfd sp!, {r1 - r3, pc}^ prefetch_abort: data_abort: irq: stmfd sp!, {r1 - r3, lr} bl main ldmfd sp!, {r1 - r3, pc}^ fiq: main_end: b main_end .data buf: .space 512 * 8 sk: .end 不過由於在這個串口程式中沒有中斷介入,所以這部分最主要的就是初始化棧(buf與ks之間的512*8Byte的棧空間),為之後的C主程式創造環境
之後便是最主要的工作:編寫C程式操作串口相關暫存器。首先需要閱讀minii2440原理圖與S3C2440的晶片手冊
從原理圖上得知,我們要用的0號串口對應的GPIO引腳是GPH2號與3號引腳


之後去晶片手冊找GPIO部分的GPH如下:
顯然我們要把GPH的四五位和六七位分別置為10,表示這兩個管腳進入uart模式
之後要配置S3C2440的uart控制器,到手冊的UART部分
ULCON0暫存器與串口通訊方式設置有關,我們只需要修改字長為八位:
UCON0暫存器則涉及收發模式、時鐘選擇等等,這裡我們只需要設置收發為polling模式:

波特率的設置則需要藉助UBRDIV0,這裡由於前面的時鐘默認選擇為pclk,也就是50MHZ,而波特率是115200,那麼按照下面的公式,得到UBRDIV0的值應該為(50000000 / (115200 * 16)) - 1 約等於26

收髮狀態暫存器為UTRSTAT0,收發內容暫存器為URXH0和UTXH0,注意,UTRSTAT0的0位為0時,接受暫存器為空,而1位為1時,發送暫存器為空,這一點區別要注意一下
因為之前我們不選擇FIFO模式,所以主要通過這一個暫存器來確定當前是否處於可以發送數據的狀態以及是否接收到了數據
下一步是編寫C程式
main.c
/* *配置串口傳輸主程式 *S3C2440 * */ #define GPHCON (*(unsigned int *)(0x56000070)) #define ULCON0 (*(unsigned int *)(0x50000000)) #define UCON0 (*(unsigned int *)(0x50000004)) #define UBRDIV0 (*(unsigned int *)(0x50000028)) #define UTXH0 (*(unsigned int *)(0x50000020)) #define URXH0 (*(unsigned int *)(0x50000024)) #define UTRSTAT0 (*(unsigned int *)(0x50000010)) void configGPH(void); void configULC(void); void configUC(void); void configUBR(void); void uart_init(void); void RTX0(void) { while (1) { if (UTRSTAT0 & 2) { break; } } UTXH0 = 'c'; int i; for (i = 1; i <= 2000000; ++i) { ; } } int main(void) { uart_init(); while (1) { RTX0(); } return 0; } void uart_init(void) { configGPH(); configULC(); configUC(); configUBR(); } void configGPH(void) //設置GPH2、3分別為TXD0,RXD0 { unsigned int GPH23 = GPHCON; GPH23 |= 1 << 5; GPH23 |= 1 << 3; GPH23 &= ~(1 << 4); GPH23 &= ~(1 << 2); GPHCON = GPH23; } void configULC(void) //設置傳輸字長為8bit { unsigned int ULC = ULCON0; ULC |= 1 << 1; ULC |= 1; ULCON0 = ULC; } void configUC(void) //設置接受與發送為polling模式 { unsigned int UC = UCON0; UC |= (1 << 2); UC |= 1; UC &= ~(1 << 3); UC &= ~(1 << 1); UCON0 = UC; } /* * 設置波特率 暫存器設置為(時鐘頻率50MHZ)/(波特率115200 * 16)-1 最後為26 */ void configUBR(void) { unsigned int UBR = UBRDIV0; UBR = 26; UBRDIV0 = UBR; }
這個程式令開發板每隔一定時間通過串口列印一個“c”字元,功能簡單但是每個步驟都要正確,特別是地址的設置以及位運算操作
之後我們要編寫連接腳本來使彙編程式與C程式按照正常順序生成組合之後的二進位文件
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf-littlearm") OUTPUT_ARCH(arm) ENTRY(_start) SECTIONS { . = 0x30001000; /*start address*/ . = ALIGN(4); .text : { start.o(.text) *(.text) } . = ALIGN(4); .data : { *(data) } . = ALIGN(4); .bss : { *(.bss) } }
這個腳本告訴連接器將start.s生成的start.o作為程式碼的開始,其中入口就是裡面的_start標號,同時設置各部分起始地址
補充一點為什麼選擇程式的起始地址為0x30001000:我的mini2440的RAM是64MB,地址是從0x30000000-0x34000000,不過在高地址運行著superboot,所以選擇大於0x30000000的附近地址段0x30001000,當然也可以選擇0x30000000,但不能接近上限。
這樣make之後,交叉編譯、處理出了uart_main.bin文件,用方孔usb和usb轉串口線連接mini2440與主機,同時打開超級終端接收串口列印資訊,連接mini2440電源,使開發板從Nor Flash啟動進入superboot,用配套的minitools工具將uart_main.bin文件寫入開發板的目標地址並運行就可以了。
超級終端出現字元,說明程式運行成功:

程式最終順利實現的關鍵是正確初始化C程式的運行環境,並且將暫存器地址逐一檢查並確認無誤,由於所涉及的暫存器在更複雜的程式中會更加繁多,所以對暫存器的準確操作尤為重要

