目錄
- STC8H開發(一): 在Keil5中配置和使用FwLib_STC8封裝庫(圖文詳解)
- STC8H開發(二): 在Linux VSCode中配置和使用FwLib_STC8封裝庫(圖文詳解)
- STC8H開發(三): 基於FwLib_STC8的模數轉換ADC介紹和演示用例說明
- STC8H開發(四): FwLib_STC8 封裝庫的介紹和使用注意事項
- STC8H開發(五): SPI驅動nRF24L01無線模塊
- STC8H開發(六): SPI驅動ADXL345三軸加速度檢測模塊
- STC8H開發(七): I2C驅動MPU6050三軸加速度+三軸角速度檢測模塊
- STC8H開發(八): NRF24L01無線傳輸音頻(對講機原型)
- STC8H開發(九): STC8H8K64U模擬USB HID外設
- STC8H開發(十): SPI驅動Nokia5110 LCD(PCD8544)
- STC8H開發(十一): GPIO單線驅動多個DS18B20數字溫度計
- STC8H開發(十二): I2C驅動AT24C08,AT24C32系列EEPROM存儲
前面介紹了如何在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文件中, 在開發時可以多看一眼, 避免不必要的時間浪費.