目錄
- STC8H開發(一): 在Keil5中配置和使用FwLib_STC8封裝庫(圖文詳解)
- STC8H開發(二): 在Linux VSCode中配置和使用FwLib_STC8封裝庫(圖文詳解)
- STC8H開發(三): 基于FwLib_STC8的模數轉換ADC介紹和演示用例說明
- STC8H開發(四): FwLib_STC8 封裝庫的介紹和注意事項
前面介紹了如何在Keil5和PlatformIO環境下使用FwLib_STC8, 還有一個ADC數模轉換的例子. 接下來整體介紹一下這個封裝庫, 以及使用這個封裝庫進行開發的注意事項. 這篇可能會不斷的更新.
關于寫 FwLib_STC8 的動機
寫這個封裝庫的初衷, 首先是避免每次在做STC8G和STC8H的開發時去查手冊, 這個是最主要的動機; 其次, 是要接近直接使用暫存器開發的效率, 不能因為引入封裝庫造成很大的資源開銷.
在 STC89/STC90 這一代, 幾十個SFR還是可以記憶的. 到了STC11, STC12, 開始出現ADC, SPI這些外設, 也還可以接受. 到STC15之后, SFR數量一下子上來, 單單PWM就有十幾個SFR, 單憑記憶就很難記住這些東西了. 并且在STC15之后, 同系列之間差異增加, 每個MCU的運行時鐘都可能不一樣, 從6MHz到40MHz可以自由設定, 就連基礎的定時器和串口設定都帶來了很大的難度.
STC-ISP工具中提供了一些代碼模板, 但是這些代碼并非完全可用, 靈活性也不夠, 例如延時方法都是不帶引數的.
早期的嘗試
如果經常在不同的MCU之間切換, 就會感覺到每次寫都像是第一次寫, 都得去查手冊去計算, 還容易出錯, 費時費力. 把一些先驗知識代碼化, 就能簡化這個程序, 用一次的時間節省將來無數時間.
邏輯代碼化
在MCS51這個場景是比較尷尬的: 片內資源太少了.
如果你把各種初始化和計算的作業都放到代碼里, 那么就會占用運行資源, 導致韌體體積增大, 運行時耗費的記憶體增加, 一些稍微復雜一點的邏輯就沒法跑了. 就像在 HML_FwLib_STC12 這個專案里的嘗試一樣, 很好用, 但是也很占資源, 一不小心就超出記憶體限制. 以至于后來將串口1初始化單獨寫了個直接寫暫存器的方法.
HML_FwLib_STC12 這個專案還存在一個問題, 就是SFR變數名與STC官方的命名不一致. 如果僅僅是在Linux下開發, 自成一體, 這個問題不是很重要, 但是如果要使用網路上其他人的代碼, 這些代碼大都是在Keil C51下開發的, 就不能使用 HML_FwLib_STC12 快速運行, 因為有很多命名需要改.
使用python工具生成代碼
所以對于STC8, 最初從另一個方向做了嘗試, 就是 stcmx 這個專案.
stcmx 這個專案是用python寫的, 在命令列中以互動的形式對各個外設進行選項設定, 然后直接生成C代碼.
生成的代碼非常簡潔, 都是對暫存器的直接賦值, 一步到位直接完成初始化. 風格是這樣的
void clock_init()
{
// [ BAH,0,0x00]: 外設埠切換控制暫存器2,串口2/3/4,I2C,比較器
P_SW2 = 0x80;
// [FE01H,1,0x00]: 時鐘分頻暫存器,ISP可能寫入預設值
CLKDIV = 0x00;
// [ 9FH,0,0x00]: IRC頻率調整暫存器, ISP可能寫入預設值, 0x75:24MHz
IRTRIM = 0x75;
// [ 9EH,0,0x00]: IRC頻率微調暫存器, ISP可能寫入預設值
LIRTRIM = 0x00;
// [ BAH,0,0x00]: 外設埠切換控制暫存器2,串口2/3/4,I2C,比較器
P_SW2 = 0x00;
}
void timer_init()
{
// [ D6H,0,0x00]: 定時器2高位元組
T2H = 0xFF;
// [ D7H,0,0x00]: 定時器2低位元組
T2L = 0xCB;
// [ 87H,0,0x30]: 電源控制暫存器
PCON = 0xB0;
// [ 8EH,0,0x01]: 輔助暫存器
AUXR = 0x15;
}
void uart_init()
{
// [ 98H,0,0x00]: 串口1控制暫存器
SCON = 0x50;
// [ 87H,0,0x30]: 電源控制暫存器
PCON = 0xB0;
// [ 8EH,0,0x01]: 輔助暫存器
AUXR = 0x15;
}
這種方式極其節省資源, 也解決了知識復用的問題, 比如我要在36.864MHz下用timer2開啟uart1, 波特率為115200, 只需要設定選項, 輸入這些數字, 直接就能得到暫存器的初始化代碼.
但是這種形式的缺點是工具本身的開發極其繁瑣, 等于要在python里面把MCU的每個暫存器每個bit的邏輯都結構化了, 還得配上文字說明, 可以認為和STM32CubeMx做的事情是類似的.
還有一個更大的缺點是不靈活, 在已經生成代碼之后, 如果需要對某些項做調整, 那么要么重新生成一遍, 要么繼續查手冊.
在寫了一段時間后, 投入太大, 逐漸放棄了這個方向.
使用宏的方式將邏輯代碼化
這就是 FwLib_STC8 這個專案的嘗試, 兼顧了靈活性和節約資源. 我從來都不喜歡宏陳述句, 但是在這個場景, 確實宏陳述句有獨特的好處.
在 FwLib_STC8 中, 90%的暫存器操作都是用宏陳述句實作的.
宏陳述句提供了一種類似于暫存器的文字注釋的功能, 在開發時的體驗類似于方法呼叫, 因為像VSCode這樣的IDE, 會代碼提示并且自動不全.
在編譯階段, 宏陳述句就會被翻譯成直接的暫存器操作, 中間節約了方法呼叫的堆疊. 沒有用到的宏陳述句不會出現在編譯結果里, 不占任何資源. 而如果你寫了函式, 函式不管呼叫沒呼叫, 只要同一個C檔案的函式被呼叫了, 這個C檔案里的所有函式都會一并出現在編譯結果里. 這樣帶來的編譯結果尺寸差異是很明顯的.
唯一比直接使用暫存器賦值更占用資源的地方, 是對SFR的直接賦值操作可能會根據配置項的不同被拆成好幾步, 但是這點overheat是值得的, 因為這樣才能實作不查手冊直接用封裝庫寫代碼, 呼叫的每一步知道自己在做什么.
現在的代碼就變成了這樣的風格
SYS_SetClock();
// UART1, baud 115200, baud source Timer2, 1T mode, interrupt on
UART1_Config8bitUart(UART1_BaudSource_Timer2, HAL_State_ON, 115200);
UART1_SetRxState(HAL_State_ON);
// Enable UART1 interrupt
EXTI_Global_SetIntState(HAL_State_ON);
EXTI_UART1_SetIntState(HAL_State_ON);
為什么不使用inline: inline不是強制inline的, 編譯器會根據情況判斷是否inline, 可能會被作為函式進行呼叫.
使用 FwLib_STC8 進行開發的方式
使用Keil C51的用戶應該會相對簡單, 因為直接將封裝庫加入專案就可以, 另外頻率可以直接用STC-ISP設定, 省掉了維護一套頻率引數的煩惱. 而在Linux下的用戶, 就需要維護一套編譯引數, 用于在程式中指定MCU頻率, 如果使用PlatformIO開發, 封裝庫已經通過library.json做了適配, 只要放入專案lib目錄, 就會自動識別并添加到include路徑.
在demo目錄下有豐富的演示示例, 基本上覆寫了全部片內外設. 另外還有對常見元件, 例如喜聞樂見的MAX7219 8x8點陣, NRF24L01無線模塊, SSD1306 OLED屏, ST7735 LCD這些設備的驅動.
翻閱一下演示代碼, 就能基本了解這個封裝庫的呼叫方法.
下面說需要注意的幾點
1. 不能隨便在引數里使用++, --這類運算式
這是宏呼叫的固有缺陷, 因為宏畢竟不是函式, 它只是字串模板, 在使用++, --這類運算子時, 會將這個操作放到模板里展開, 如果在模板里對這個變數參考了兩次, 那么它就會執行兩次, 這會造成意想不到的問題.
2. 如果要同時對Keil C51和SDCC兼容, 就必須使用封裝庫提供的宏定義
封裝庫中引入了一些宏定義, 用于保證對 Keil C51 和 SDCC 的兼容性.
命名和形式來源于 sdcc compiler.h.
如果你希望代碼在 Keil C51 和 SDCC 下都能編譯, 在編碼時就應當使用這些宏, 而不是編譯器對應的關鍵詞.
以下是相關的宏定義串列
| Macro | Keil C51 | SDCC |
|---|---|---|
| __BIT | bit | __bit |
| __IDATA | idata | __idata |
| __PDATA | pdata | __pdata |
| __XDATA | xdata | __xdata |
| __CODE | code | __code |
| SBIT(name, addr, bit) | sbit name = addr^bit | __sbit __at(addr+bit) name |
| SFR(name, addr) | sfr name = addr | __sfr __at(addr) name |
| SFRX(addr) | (*(unsigned char volatile xdata *)(addr)) | (*(unsigned char volatile __xdata *)(addr)) |
| SFR16X(addr) | (*(unsigned int volatile xdata *)(addr)) | (*(unsigned int volatile __xdata *)(addr)) |
| INTERRUPT(name, vector) | void name (void) interrupt vector | void name (void) __interrupt (vector) |
| INTERRUPT_USING(name, vector, regnum) | void name (void) interrupt vector using regnum | void name (void) __interrupt (vector) __using (regnum) |
| NOP() | _nop_() | __asm NOP __endasm |
這些宏定義可以在 include/fw_reg_base.h 中查看
3. 部分宏陳述句的引數是列舉, 呼叫時要留意
使用宏陳述句的一個缺點就是沒有型別提示, 雖然在變數名上我已經盡量體現出這個引數的型別, 但是寫代碼時, IDE是沒有提示的. 所以這里需要注意的是, 有一些輸入引數是列舉, 在呼叫時最好切換到宣告這個宏的.h檔案中看一眼, 這些列舉一般都定義在.h檔案的開始部分.
4. 不同MCU之間的資源差異
封裝庫本身只區分了STC8G和STC8H兩個大類, 例如STC8G 有 PCA但是沒有PWM, STC8H 中有PWM沒有PCA. 在大類的內部, 例如 STC8H 的各個子系列, 在功能上也是有差異的, 例如 STC8H1K 系列的ADC是 10bit, STC8H3K, STC8H8K 的ADC是12bit, 還有通道的數量以及和IO口的映射關系都有區別.
這些區別基本上都列在了對應外設的.h檔案中, 在開發時可以多看一眼, 避免不必要的時間浪費.
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/412793.html
標籤:嵌入式
