Lab4 : Bootloader - 沒有l(wèi)oad只有g(shù)o

實驗環(huán)境依然是windows + CubeMX + Keil v5。

實驗連接圖

連接圖

連接圖與Lab3基本一致,ST-LINK接四根線3.3V、GND、SWDIO、SWCLK分別對應(yīng)STM32板子上的3.3V、GND、DIO、DCLK。此為燒錄用的線路。而PA9、PA10為串口通信所用的線路。

實驗步驟

0. 串口收發(fā)

由于要通過串口發(fā)送命令至STM32板子,首先需要解決的是串口收發(fā)的問題。

串口的發(fā)送在上一個實驗中已經(jīng)有寫過相關(guān)代碼。而串口接收需要進行一些設(shè)置。串口接收我采用的是中斷接收至環(huán)形緩沖區(qū)的方式處理,而不是使用自動機。

首先,串口接收需要中斷的支持。與Lab3中的做法一樣,需要在初始化的時候打開中斷,并設(shè)置相應(yīng)的中斷處理函數(shù)。所以在uart_init()中,打開了USART1的中斷,并設(shè)置相應(yīng)的優(yōu)先級。并覆蓋函數(shù)USART1_IRQHandler()處理中斷。

而不同的是,在Cube庫的封裝方式下,該中斷并不單單由中斷的回調(diào)函數(shù)構(gòu)成,而是需要用戶手動定義串口信息的接收位置。原因是在接收由串口發(fā)來的信息的時候,程序需要預(yù)先知道將信息放在何處,而同時用戶需要保證在接收的時候目標地址有足夠的空間存放信息。

函數(shù)末尾所用的HAL_UART_Receive_IT函數(shù)就是起著設(shè)置信息存放位置的作用,三個參數(shù)分別表示接收信息的UART句柄,接收信息的Buffer地址以及接受信息長度。該函數(shù)在接收到信息之后,會在Buffer指向的地址順序?qū)懭胱址?,并在達到指定長度之后調(diào)用回調(diào)函數(shù)HAL_UART_RxCpltCallback。

以上的準備工作結(jié)束后,串口接收信息的中斷響應(yīng)已經(jīng)結(jié)束。此時需要在信息讀取之后進行一些處理,此時覆蓋函數(shù)HAL_UART_RxCpltCallback并在其中處理邏輯即可。

整個中斷處理的流程是USART1_IRQHandler ---> HAL_UART_IRQHandler ---使用預(yù)先使用HAL_UART_Receive_IT設(shè)置好的值---> HAL_UART_RxCpltCallback完成接收。

#define BUFFSIZE 512
#define BACKSPACE 127
#define ENTER '\r'
char str[100] = "Uart";
struct uart {  
    uint8_t *rear;
    uint8_t *front;
};
uint8_t aRxBuffer[BUFFSIZE];  // 接收緩沖區(qū)數(shù)組
struct uart uart_rev; // 接收緩沖區(qū)頭尾指針

void SerialPutchar(char s){
    HAL_UART_Transmit(&huart1, (uint8_t*)&s, 1, 500);
}

// 環(huán)形緩沖區(qū)的指針加
void ptrInc(uint8_t **ptr, uint8_t* base, int len){
    *ptr += 1;
    if (*ptr >= base + len)
        *ptr = base;
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *UartHandle)  
{  
    uint8_t ret = HAL_OK;
    
    // 在這個位置,上一次調(diào)用HAL_UART_Receive_IT使用的位置已經(jīng)被填充了接收的信息
    char c = *uart_rev.rear;
    // 我的串口軟件putty在按鍵的時候默認將所有信息直接發(fā)送給板子
    // 所以板子在接收到信息后需要在屏幕上進行顯示
    SerialPutchar(c);
    // 特別的,如果用戶按下回車,會讀到'\r'字符
    // 而為了達到常見的回車效果,需要再補上'\n'
    if (c == '\r'){
        SerialPutchar('\n');
    }
    
    // 預(yù)處理完讀入的信息之后,將緩沖區(qū)的指針進行移動
    ptrInc(&uart_rev.rear, aRxBuffer, BUFFSIZE);
    if (uart_rev.rear == uart_rev.front)
        ptrInc(&uart_rev.front, aRxBuffer, BUFFSIZE);
    
    // 重新設(shè)置讀取的位置以及讀取的長度
    do{  
        ret = HAL_UART_Receive_IT(UartHandle, uart_rev.rear, 1);  
    }while(ret != HAL_OK);
}

void USART1_IRQHandler(void){
    //調(diào)用HAL函數(shù)庫自帶的處理函數(shù)
    HAL_UART_IRQHandler(&huart1);
}

void uart_init(uint32_t BaudRate)  
{  
...  
    __HAL_UART_ENABLE(&huart1);  
  
    // 打開中斷
    NVIC_SetPriority(USART1_IRQn, 0);
    NVIC_EnableIRQ(USART1_IRQn);    
...
    // 設(shè)置信息填充位置
    if (HAL_UART_Receive_IT(&huart1, (uint8_t*)aRxBuffer, 1) != HAL_OK){  
        Error_Handler();  
    }
}

以上代碼通過對中斷進行處理,將串口的信息讀入到環(huán)形緩沖區(qū)內(nèi)存放。而程序要使用的時候,直接進行讀取即可。以下是一些對串口讀取進行封裝的函數(shù)。

// 嘗試著在指定時間@time_out內(nèi)讀入單個字符并放置在fmt所指向的位置
// 讀取成功返回0,不成功返回-1
int8_t uart_read(uint8_t *fmt, uint16_t time_out){  
    while(time_out){  
        if(uart_rev.front != uart_rev.rear){  
            *fmt=*uart_rev.front;
            ptrInc(&uart_rev.front, aRxBuffer, BUFFSIZE);
            return 0;  
        }  
    time_out--;  
    }  
    return (int8_t)-1;  
}

// 阻塞式的從緩沖區(qū)中讀取最長為@upperBound的一行字符串
// 結(jié)果儲存于@fmt所指向的位置,返回值為讀取的字符個數(shù)
int8_t uart_gets(uint8_t *fmt, uint16_t upperBound)  
{  
    int count = 0;
    // 為字符串結(jié)尾'\0'騰出空間
    upperBound -= 1;
    // 阻塞讀取
    while(count < upperBound){
        // 每當緩沖區(qū)內(nèi)有字符則進行處理
        if(uart_rev.front != uart_rev.rear){
            // 讀入字符并把緩沖區(qū)頭指針前移
            char c = *uart_rev.front;
            ptrInc(&uart_rev.front, aRxBuffer, BUFFSIZE);
            // 如果為ENTER表示行讀取完畢
            if (c == ENTER){
                break;
            }
            // 作為正常字符存入fmt中
            *fmt = c;
            // 正常情況下默認指針加一,如果讀入了退格號則回退
            if (c != BACKSPACE){
                fmt++;
                count++;
            }else if(count > 0){
                fmt--;
                count--;
            }
        }
    }
    // 字符串結(jié)尾0
    *fmt = '\0';
    return count;  
}

在程序運行中,可以利用上述封裝的函數(shù)實現(xiàn)一個終端命令行程序。

    while (1) {
        int count = 0;
        char s[100];
        SerialPuts("LM STM32 > ");
        count = uart_gets((uint8_t*)str, 100);
        count = sprintf(s, "Readin: %d -%s-\r\n", count, str);
        HAL_UART_Transmit(&huart1, (uint8_t*)s, count, 500);
    }
運行結(jié)果

1. 串口命令實現(xiàn)

使用上節(jié)所用的uart_gets函數(shù)獲取每次輸入的一整行命令,根據(jù)命令作出相應(yīng)操作即可。

值得一提的是poke指令,隨意給定地址亂寫數(shù)據(jù)很容易會把整個程序?qū)懕馈榱朔奖銣y試,開了一個buff數(shù)組并給出相應(yīng)的地址及長度。

    int buff[100];
    // 輸出buff數(shù)組的地址
    buff[0] = sprintf(str, "Buffer Addr: %p Len: %d\r\n", buff, 100);
    HAL_UART_Transmit(&huart1, (uint8_t*)str, buff[0], 500);
    while (1) {
        int count = 0;
        char s[100];
        char cmd[100];
        SerialPuts("LM STM32 > ");
        // 從串口輸入命令
        count = uart_gets((uint8_t*)str, 100);
        count = sprintf(s, "Readin: %d -%s-\r\n", count, str);
        HAL_UART_Transmit(&huart1, (uint8_t*)s, count, 500);

        // 讀出第一個表示指令的字符串,并據(jù)此執(zhí)行命令
        sscanf(str, "%s", cmd);
        if (strcmp(cmd, PEEK) == 0){
            int addr = 0, readAns;
            // peek指令需要一個16進制數(shù),表示地址,同時不能有更多的參數(shù)
            // 此處使用%s避免后續(xù)的多余參數(shù)
            readAns = sscanf(str + PEEKLEN, "%x %s", &addr, s);
            if (readAns == 1){
                sprintf(s, PEEK": %x %x\r\n", addr, *((int*)addr));
                SerialPuts(s);
            }else{
                SerialPuts(ERROR);
            }
        }else if(strcmp(cmd, POKE) == 0){
            int addr = 0, data = 0, readAns;
            // poke需要兩個16進制數(shù),分別表示地址與寫入的數(shù)據(jù)
            readAns = sscanf(str + POKELEN, "%x %x %s", &addr, &data, s);
            if (readAns == 2){
                *((int*)addr) = data;
                sprintf(s, POKE": %x %x\r\n", addr, *((int*)addr));
                SerialPuts(s);
            }else{
                SerialPuts(ERROR);
            }
        }
    }
串口執(zhí)行命令

2. GO指令實現(xiàn)

話接上回,一個命令行框架已經(jīng)基本搭出,go指令也只是在其上進行一些改動即可。

go指令最重要的是程序執(zhí)行的功能。而實際上,程序執(zhí)行所需要的僅僅只是找到入口,初始化好全局的變量之后,跳入入口即可。而用戶程序的燒錄使用的不是串口,而是使用例如說ST/Link燒錄的方式寫入Flash中。

#define GO "go"
#define GOLEN strlen(GO)
#define APP_ADDR 0x08010000

typedef void (*iapfun)(void);
iapfun jump2app;

void iap_load_app(int appxaddr){
    if(((*(int*)appxaddr)&0x2FFE0000)==0x20000000){ //檢查棧頂?shù)刂肥欠窈戏?        jump2app = (iapfun)*(int*)(appxaddr+4); //用戶代碼區(qū)第二個字為程序開始地址(復(fù)位地址),此處查看中斷向量表可知
        __set_MSP(appxaddr); //初始化APP堆棧指針(用戶代碼區(qū)的第一個字用于存放棧頂?shù)刂?
        jump2app(); //跳轉(zhuǎn)到APP,執(zhí)行復(fù)位中斷程序
    }
}

int main(){
    ...
    int buff[100];
    buff[0] = sprintf(str, "Buffer Addr: %p Len: %d\r\n", buff, 100);
    HAL_UART_Transmit(&huart1, (uint8_t*)str, buff[0], 500);
    buff[1] = sprintf(str, "USER PROGRAM\r\n");
    //buff[1] = sprintf(str, "BOOTLOADER\r\n"); // 便于區(qū)分是否發(fā)生了跳轉(zhuǎn)
    HAL_UART_Transmit(&huart1, (uint8_t*)str, buff[1], 500);
    while (1){
        ...
        if ( ... ){
            ...
        }else if(strcmp(cmd, GO) == 0){
            int addr, readAns;
            readAns = sscanf(str + GOLEN, "%x %s", &addr, s);
            if (readAns == -1){
                addr = APP_ADDR;
            }
            if (readAns <= 1){
                iap_load_app(addr);
            }else{
                SerialPuts(ERROR);
            }
        }
    }
    ...
}

而此時,用戶程序的來源還是上位機直接燒錄。所以,上位機需要燒錄兩個程序至不同的位置。通過Flash -> Configure Flash Tools -> Utilities -> Settings 中將下載選項選擇至只擦除對應(yīng)的塊防止程序被擦除。

Settings

而在下方需要點擊Add添加一個在用戶地址的Programming Algorithm。否則會出現(xiàn)這樣的錯誤提示

錯誤提示

接下來只需要調(diào)整程序下載的位置以及程序內(nèi)部的標識碼即可下載兩個程序。

BootLoader
User Program

調(diào)整好下載位置并進行下載后,即可開始進行測試。

運行結(jié)果

以bootloader作為用戶程序有一個好處,就是從bootloader跳出去之后………… 誒, 你還可以再跳回來!誒,跳回來!啊……真是一個無用的特性?。?/p>

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容