實驗環(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);
}

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);
}
}
}

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)的塊防止程序被擦除。

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

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


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

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