Lab4 : Bootloader - 没有load只有go

实验环境依然是windows + CubeMX + Keil v5。

实验连接图

连接图

连接图与Lab3基本一致,ST-LINK接四根线3.3V、GND、SWDIO、SWCLK分别对应STM32板子上的3.3V、GND、DIO、DCLK。此为烧录用的线路。而PA9、PA10为串口通信所用的线路。

实验步骤

0. 串口收发

由于要通过串口发送命令至STM32板子,首先需要解决的是串口收发的问题。

串口的发送在上一个实验中已经有写过相关代码。而串口接收需要进行一些设置。串口接收我采用的是中断接收至环形缓冲区的方式处理,而不是使用自动机。

首先,串口接收需要中断的支持。与Lab3中的做法一样,需要在初始化的时候打开中断,并设置相应的中断处理函数。所以在uart_init()中,打开了USART1的中断,并设置相应的优先级。并覆盖函数USART1_IRQHandler()处理中断。

而不同的是,在Cube库的封装方式下,该中断并不单单由中断的回调函数构成,而是需要用户手动定义串口信息的接收位置。原因是在接收由串口发来的信息的时候,程序需要预先知道将信息放在何处,而同时用户需要保证在接收的时候目标地址有足够的空间存放信息。

函数末尾所用的HAL_UART_Receive_IT函数就是起着设置信息存放位置的作用,三个参数分别表示接收信息的UART句柄,接收信息的Buffer地址以及接受信息长度。该函数在接收到信息之后,会在Buffer指向的地址顺序写入字符,并在达到指定长度之后调用回调函数HAL_UART_RxCpltCallback。

以上的准备工作结束后,串口接收信息的中断响应已经结束。此时需要在信息读取之后进行一些处理,此时覆盖函数HAL_UART_RxCpltCallback并在其中处理逻辑即可。

整个中断处理的流程是USART1_IRQHandler ---> HAL_UART_IRQHandler ---使用预先使用HAL_UART_Receive_IT设置好的值---> 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];  // 接收缓冲区数组
struct uart uart_rev; // 接收缓冲区头尾指针

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

// 环形缓冲区的指针加
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;
    
    // 在这个位置,上一次调用HAL_UART_Receive_IT使用的位置已经被填充了接收的信息
    char c = *uart_rev.rear;
    // 我的串口软件putty在按键的时候默认将所有信息直接发送给板子
    // 所以板子在接收到信息后需要在屏幕上进行显示
    SerialPutchar(c);
    // 特别的,如果用户按下回车,会读到'\r'字符
    // 而为了达到常见的回车效果,需要再补上'\n'
    if (c == '\r'){
        SerialPutchar('\n');
    }
    
    // 预处理完读入的信息之后,将缓冲区的指针进行移动
    ptrInc(&uart_rev.rear, aRxBuffer, BUFFSIZE);
    if (uart_rev.rear == uart_rev.front)
        ptrInc(&uart_rev.front, aRxBuffer, BUFFSIZE);
    
    // 重新设置读取的位置以及读取的长度
    do{  
        ret = HAL_UART_Receive_IT(UartHandle, uart_rev.rear, 1);  
    }while(ret != HAL_OK);
}

void USART1_IRQHandler(void){
    //调用HAL函数库自带的处理函数
    HAL_UART_IRQHandler(&huart1);
}

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

以上代码通过对中断进行处理,将串口的信息读入到环形缓冲区内存放。而程序要使用的时候,直接进行读取即可。以下是一些对串口读取进行封装的函数。

// 尝试着在指定时间@time_out内读入单个字符并放置在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;  
}

// 阻塞式的从缓冲区中读取最长为@upperBound的一行字符串
// 结果储存于@fmt所指向的位置,返回值为读取的字符个数
int8_t uart_gets(uint8_t *fmt, uint16_t upperBound)  
{  
    int count = 0;
    // 为字符串结尾'\0'腾出空间
    upperBound -= 1;
    // 阻塞读取
    while(count < upperBound){
        // 每当缓冲区内有字符则进行处理
        if(uart_rev.front != uart_rev.rear){
            // 读入字符并把缓冲区头指针前移
            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--;
            }
        }
    }
    // 字符串结尾0
    *fmt = '\0';
    return count;  
}

在程序运行中,可以利用上述封装的函数实现一个终端命令行程序。

    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);
    }
Lab4 : Bootloader - 没有load只有go_第1张图片
运行结果

1. 串口命令实现

使用上节所用的uart_gets函数获取每次输入的一整行命令,根据命令作出相应操作即可。

值得一提的是poke指令,随意给定地址乱写数据很容易会把整个程序写崩。为了方便测试,开了一个buff数组并给出相应的地址及长度。

    int buff[100];
    // 输出buff数组的地址
    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);

        // 读出第一个表示指令的字符串,并据此执行命令
        sscanf(str, "%s", cmd);
        if (strcmp(cmd, PEEK) == 0){
            int addr = 0, readAns;
            // peek指令需要一个16进制数,表示地址,同时不能有更多的参数
            // 此处使用%s避免后续的多余参数
            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进制数,分别表示地址与写入的数据
            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);
            }
        }
    }
Lab4 : Bootloader - 没有load只有go_第2张图片
串口执行命令

2. GO指令实现

话接上回,一个命令行框架已经基本搭出,go指令也只是在其上进行一些改动即可。

go指令最重要的是程序执行的功能。而实际上,程序执行所需要的仅仅只是找到入口,初始化好全局的变量之后,跳入入口即可。而用户程序的烧录使用的不是串口,而是使用例如说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){ //检查栈顶地址是否合法
        jump2app = (iapfun)*(int*)(appxaddr+4); //用户代码区第二个字为程序开始地址(复位地址),此处查看中断向量表可知
        __set_MSP(appxaddr); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
        jump2app(); //跳转到APP,执行复位中断程序
    }
}

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"); // 便于区分是否发生了跳转
    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 中将下载选项选择至只擦除对应的块防止程序被擦除。

Lab4 : Bootloader - 没有load只有go_第3张图片
Settings

而在下方需要点击Add添加一个在用户地址的Programming Algorithm。否则会出现这样的错误提示

Lab4 : Bootloader - 没有load只有go_第4张图片
错误提示

接下来只需要调整程序下载的位置以及程序内部的标识码即可下载两个程序。

BootLoader
Lab4 : Bootloader - 没有load只有go_第5张图片
User Program

调整好下载位置并进行下载后,即可开始进行测试。

Lab4 : Bootloader - 没有load只有go_第6张图片
运行结果

以bootloader作为用户程序有一个好处,就是从bootloader跳出去之后………… 诶, 你还可以再跳回来!诶,跳回来!啊……真是一个无用的特性啊!

参考资料

  • STM32L0xx_HAL_Driver库的使用——UART
  • STM32L0串口求助!!!!急求,调试了好久了
  • STM32F0 UART – TUTORIAL 5
  • USART HAL Receive IT Not working
  • STM32之bootloader
  • [经验] STM32的bootloader IAP编程(转载总结)
  • 手把手教你写STM32的bootloader(SDIO读取TF更新Bootloader)
  • stm32 Bootloader设计(YModem协议)

你可能感兴趣的:(Lab4 : Bootloader - 没有load只有go)