閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷

В 第一部分 我試著告訴那些從小就喜歡 Arduino 的電子工程師,他們應該如何以及為什麼閱讀微控制器的資料表和其他文件。 文本很大,所以我承諾在另一篇文章中展示實際範例。 嗯,他自稱是牛奶蘑菇…

今天我將向您展示如何使用資料表來解決 STM32(Blue Pill)和 STM8 控制器上的許多項目、任務,非常簡單,但必要。 所有演示項目都專門針對我最喜歡的 LED;我們將大量點亮它們,為此我們將不得不使用各種有趣的外圍設備。

文字再次變得很大,所以為了方便起見,我製作了內容:

STM32 Blue Pill:16 個 LED,帶 DM634 驅動器
STM8:設定XNUMX個PWM引腳
STM8:三個腳位上的 8 個 RGB LED,中斷

免責聲明:我不是工程師,我不會假裝對電子學有很深的了解,這篇文章是為像我這樣的業餘愛好者而寫的。 事實上,兩年前我就將自己視為目標受眾。 如果當時有人告訴我,陌生晶片上的資料表讀起來並不可怕,我就不會花很多時間在網路上尋找一些程式碼片段並發明剪刀和膠帶拐杖。

本文的重點是資料表,而不是項目,因此程式碼可能不是很整潔並且常常很局促。 這些項目本身非常簡單,但適合初次接觸新晶片。

我希望我的文章能夠幫助處於類似階段的沉浸在這個愛好中的人。

STM32

16 個 LED,附 DM634 和 SPI

使用 Blue Pill (STM32F103C8T6) 和 DM634 LED 驅動器的小型專案。 使用資料表,我們將找出驅動程式、STM IO 連接埠並配置 SPI。

DM634

台灣晶片,16路16位PWM輸出,可串接。 低階12位型號是從國內一個專案得知的 輕包裝。 有一次,在DM63x和大家熟知的TLC5940之間進行選擇時,我選擇DM有幾個原因:1)速賣通上的TLC肯定是假的,但這個不是; 2) DM有一個自主的PWM,有自己的頻率產生器; 3)它可以在莫斯科以便宜的價格購買,而不用等待阿里的包裹。 當然,學習如何自己控制晶片而不是使用現成的庫是很有趣的。 晶片現在主要採用 SSOP24 封裝;它們很容易焊接到適配器上。

由於製造商是台灣人, 數據表 該晶片是用中文英文寫的,這意味著它會很有趣。 首先我們來看看引腳排列(引腳連接)以了解連接哪一條腿,以及引腳的描述(引腳說明)。 16 針:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
直流灌源(漏極開路)

廚盆 / 開漏輸出 - 流走; 流入電流源; 輸出在活動狀態下接地 - LED 透過陰極連接到驅動器。 從電氣角度來說,這當然不是「漏極開路」(漏極開路),但在資料表中經常會發現漏極模式引腳的這種指定。

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
REXT 和 GND 之間的外部電阻用於設定輸出電流值

REXT 接腳和接地之間安裝了一個參考電阻,用於控制輸出的內阻,請參閱資料表第 9 頁的圖表。 在DM634中,這個電阻也可以透過軟體控制,設定整體亮度(全域亮度); 我不會在本文中詳細介紹,我只是在這裡放置一個 2.2 - 3 kOhm 的電阻。

為了了解如何控制晶片,我們來看看設備介面的說明:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷

是的,這就是中文英文的全部輝煌。 翻譯這個是有問題的,如果你願意的話可以理解它,但是還有另一種方法——看看數據表中如何描述與功能類似的TLC5940的連接:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
....只需三個引腳即可將資料輸入設備。 SCLK 訊號的上升沿將資料從 SIN 接腳移至內部暫存器。 所有資料載入完畢後,一個短的高 XLAT 訊號將順序傳輸的資料鎖存到內部暫存器。 內部暫存器是由 XLAT 訊號電平觸發的閘。 所有資料首先傳輸最高有效位元。

鎖存器 – 閂鎖/閂鎖/鎖。
上升沿 – 脈衝前緣
MSB優先 – 最重要(最左邊)位向前。
時鐘數據 – 順序傳輸資料(逐位)。

經常在晶片的文檔中找到,並以各種方式翻譯,因此為了理解,我將允許自己

一個小型的教育項目LED 驅動器本質上是一個移位暫存器。 「轉移」 ()在名稱中 - 設備內部資料的位元移動:推入內部的每個新位元都會將整個鏈向前推動。 由於沒有人願意在換檔期間觀察 LED 的混亂閃爍,因此該過程發生在透過阻尼器與工作暫存器分開的緩衝暫存器中()是一種等待室,其中的位元依所需的順序排列。 當一切準備好後,快門打開,零件開始工作,替換先前的批次。 單字 在微電路的文檔中幾乎總是暗示這樣的阻尼器,無論它以何種組合使用。

因此,向 DM634 的資料傳輸是這樣進行的:將 DAI 輸入設定為遠端 LED 的最高有效位的值,拉高和拉低 DCK; 將DAI輸入設定為下一位的值,拉動DCK; 依此類推,直到所有位元均已傳輸(打卡),之後我們拉 LAT。 這可以手動完成(位元爆炸),但最好使用為此專門定制的 SPI 接口,因為它在我們的 STM32 上有兩個副本。

藍丸STM32F103

簡介:STM32 控制器比 Atmega328 複雜得多,比它們看起來可怕的多。 此外,出於節能的原因,幾乎所有周邊在啟動時都被關閉,且時脈頻率為來自內部來源的8 MHz。 幸運的是,STM 程式設計師編寫了程式碼,使晶片達到了「計算出的」72 MHz,而我所知道的所有 IDE 的作者都將其包含在初始化過程中,因此我們不需要時脈(但是 如果你真的想要的話你可以)。 但您必須打開外圍設備。

文件:Blue Pill配備了流行的STM32F103C8T6晶片,有兩個有用的文件:

在數據表中,我們可能感興趣:

  • 腳位排列 – 晶片接腳排列 – 如果我們決定自己製作電路板;
  • 記憶體映射-特定晶片的記憶體映射。 參考手冊有整條線的地圖,它提到了我們沒有的暫存器。
  • 引腳定義表-列出引腳的主要功能和替代功能; 對於“藍色藥丸”,您可以在互聯網上找到更方便的圖片,其中包含引腳及其功能列表。 因此,我們立即谷歌搜尋 Blue Pill 引腳排列,並將這張圖片放在手邊:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
註:圖片來自網絡,有錯誤,已在評論中指出,謝謝。 圖片已被替換,但這是一個教訓 - 最好不要從數據表中檢查資訊。

我們刪除資料表,打開參考手冊,從現在開始我們只使用它。
流程:我們處理標準輸入/輸出,配置SPI,開啟必要的周邊。

輸入輸出

在 Atmega328 上,I/O 的實作極為簡單,這就是為什麼豐富的 STM32 選項可能會令人困惑。 現在我們只需要結論,但即使是這些也有四個選擇:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
開漏、推挽、交替推挽、交替開漏

「拉推」(推拉)是 Arduino 的通常輸出,該引腳可以採用高電平或低電平值。 但對於「漏極開路」有 困難,儘管事實上這裡一切都很簡單:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
輸出配置 / 當連接埠指派為輸出: / 輸出緩衝器啟用: / – 開漏模式:輸出暫存器中的「0」啟用 N-MOS,輸出暫存器中的「1」使連接埠處於 Hi-Z 模式( P -MOS 未啟動) / – 推挽模式:輸出暫存器中的「0」啟動N-MOS,輸出暫存器中的「1」啟動P-MOS。

開漏極之間的所有差異(漏極開路)來自「推拉」(推拉)的問題是第一個引腳不能接受高電平狀態:當向輸出暫存器寫入一個時,它會進入高電阻模式(高阻抗, 高阻抗)。 當寫入零時,引腳在兩種模式下的行為相同,無論是邏輯上還是電氣上。

在正常輸出模式下,引腳僅廣播輸出暫存器的內容。 在「替代」中,它由相應的周邊控制(見9.1.4):

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
如果連接埠位元被配置為複用功能引腳,則引腳暫存器將被停用,並且該引腳將連接到週邊引腳。

每個引腳的替代功能描述於 引腳定義 數據表位於下載的圖像上。 對於如果一個引腳有多個替代功能該怎麼辦的問題,資料表中的腳註給了答案:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
如果多個週邊裝置使用相同引腳,為了避免替代功能之間的衝突,一次只能使用一個週邊裝置,並使用外設時脈啟用(在對應的 RCC 暫存器中)進行切換。

最後,輸出模式的引腳也有時脈速度。 這是另一個節能功能;在我們的例子中,我們只是將其設為最大值然後忘記它。

所以:我們使用SPI,這意味著兩個引腳(帶有數據和帶有時脈信號)應該是“替代推挽功能”,另一個(LAT)應該是“常規推挽”。 但在分配它們之前,讓我們先處理一下 SPI。

SPI

另一個小型教育項目

SPI 或 Serial Peripheral Interface(串行外設接口)是一種簡單且非常有效的接口,用於連接 MK 與其他 MK 以及外部世界。 其工作原理已經在上面介紹過中文LED驅動器(在參考手冊中,請參閱第25節)。 SPI 可以在主機(“master”)和從機(“slave”)模式下運作。 SPI 有四個基本通道,其中並非全部都可以使用:

  • MOSI,主機輸出/從機輸入:此引腳在主機模式下發送數據,在從機模式下接收數據;
  • MISO,Master Input/Slave Output:相反,在主機中接收,在從機中發送;
  • SCK,串列時脈:設定主機中資料傳輸的頻率或從機中接收時脈訊號。 本質上是敲擊節拍;
  • SS,奴隸選擇:在這個通道的幫助下,奴隸知道有人想要他的東西。 在 STM32 上稱為 NSS,其中 N = 負數,即如果該通道接地,控制器將成為從機。 它與開漏輸出模式結合得很好,但那是另一個故事了。

與其他事物一樣,STM32 上的 SPI 功能豐富,這使得它有些難以理解。 例如,它不僅可以與SPI配合使用,還可以與I2S介面配合使用,而在文件中它們的描述是混雜的,需要及時剪掉多餘的部分。 我們的任務非常簡單:我們只需要使用 MOSI 和 SCK 發送資料。 我們轉到第25.3.4節(半雙工通信,半雙工通信),在那裡我們發現 1個時鐘和1個單向資料線 (1個時脈訊號和1個單向資料流):

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
在此模式下,應用程式在僅發送或僅接收模式下使用 SPI。 / 僅發送模式與雙工模式類似:資料在發送引腳(主模式下的 MOSI 或從模式下的 MISO)上傳輸,接收引腳(分別為 MISO 或 MOSI)可用作常規 I/O 引腳。 在這種情況下,應用程式只需忽略 Rx 緩衝區(如果讀取該緩衝區,則那裡不會有傳輸的資料)。

太好了,MISO 引腳是空閒的,讓我們將 LAT 訊號連接到它。 我們來看看Slave Select,在STM32上可以透過程式來控制,極為方便。 我們在第 25.3.1 SPI 概述中讀到同名段落:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
軟體控制 NSS(SSM = 1)/從機選擇資訊包含在 SPI_CR1 暫存器的 SSI 位元中。 外部 NSS 引腳仍可用於其他應用需求。

是時候寫入暫存器了。 我決定使用 SPI2,在資料表中尋找其基址 - 在第 3.3 節記憶體映射中:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷

好吧,讓我們開始吧:

#define _SPI2_(mem_offset) (*(volatile uint32_t *)(0x40003800 + (mem_offset)))

開啟第 25.3.3 節,其標題不言自明「在主模式下配置 SPI」:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷

1. 使用 SPI_CR2 暫存器中的 BR[0:1] 位元設定串行時脈頻率。

這些暫存器收集在同名的參考手冊部分。 地址移位(地址偏移量)對於 CR1 – 0x00,預設所有位元都被清除(重置值 0x0000):

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷

BR 位元設定控制器時脈分頻器,從而確定 SPI 的運作頻率。 我們的 STM32 頻率為 72 MHz,根據其資料表,LED 驅動器的工作頻率高達 25 MHz,因此我們需要除以四 (BR[2:0] = 001)。

#define _SPI_CR1 0x00

#define BR_0        0x0008
#define BR_1        0x0010
#define BR_2        0x0020

_SPI2_ (_SPI_CR1) |= BR_0;// pclk/4

2. 設定 CPOL 和 CPHA 位元以定義資料傳輸和序列時脈時序之間的關係(請參閱第 240 頁的圖表)

由於我們在這裡閱讀的是資料表而不是原理圖,因此讓我們仔細看看第 704 頁上的 CPOL 和 CPHA 位元的文字描述(SPI 概述):

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
時鐘相位和極性
使用 SPI_CR1 暫存器的 CPOL 和 CPHA 位元,您可以透過程式設計方式選擇四種時序關係。 當沒有資料傳輸時,CPOL(時脈極性)位元控制時脈訊號的狀態。 此位控制主從模式。 如果 CPOL 重位,則 SCK 腳位在休眠模式下為低電位。 如果 CPOL 位元被置位,則 SCK 引腳在休息模式期間為高電位。
當 CPHA(時脈相位)位置位時,高位陷阱選通是 SCK 訊號的第二個邊緣(如果 CPOL 清零則下降,如果 CPOL 置位則上升)。 數據由時脈訊號的第二次變化捕獲。 如果 CPHA 位元清零,則高位陷阱選通是 SCK 訊號的上升沿(若 CPOL 置 XNUMX,則為下降沿;若 CPOL 被清零,則為上升沿)。 數據在時脈訊號第一次變化時被捕獲。

吸收了這些知識後,我們得出結論:這兩個位必須保持為零,因為我們希望 SCK 訊號在不使用時保持低電平,並在脈衝的上升沿傳輸資料(見圖 XNUMX)。 上升沿 在 DM634 資料表中)。

順便說一下,在這裡我們第一次遇到了ST資料表中詞彙的一​​個特點:其中寫著「將位元重置為零」這句話 重置一點而且不 清除一點,例如 Atmega。

3.設定DFF位,判斷資料塊是8位還是16位格式

我特意選擇了16位元DM634,以免像DM12那樣費心傳輸633位元PWM資料。 將 DFF 設定為 XNUMX 是有意義的:

#define DFF         0x0800

_SPI2_ (_SPI_CR1) |= DFF; // 16-bit mode

4. 配置SPI_CR1暫存器中的LSBFIRST位元來決定區塊格式

LSBFIRST,顧名思義,配置傳輸時先使用最低有效位元。 但DM634希望從最高有效位開始接收資料。 因此,我們將其重置。

5. 在硬體模式下,如果需要從 NSS 引腳輸入,請在整個位元組傳輸序列期間向 NSS 引腳施加高電平訊號。 在 NSS 軟體模式下,設定 SPI_CR1 暫存器中的 SSM 和 SSI 位元。 如果 NSS 引腳用作輸出,則只需設定 SSOE 位元即可。

安裝SSM和SSI以忘記NSS硬體模式:

#define SSI         0x0100
#define SSM         0x0200

_SPI2_ (_SPI_CR1) |= SSM | SSI; //enable software control of SS, SS high

6. 必須設定 MSTR 和 SPE 位元(僅當 NSS 訊號為高電位時它們才保持設定狀態)

實際上,透過這些位,我們將 SPI 指定為主機並將其開啟:

#define MSTR        0x0004
#define SPE         0x0040

_SPI2_ (_SPI_CR1) |= MSTR; //SPI master
//когда все готово, включаем SPI
_SPI2_ (_SPI_CR1) |= SPE;

SPI 配置完畢,讓我們立即編寫向驅動程式發送位元組的函數。 繼續閱讀25.3.3“在主模式下配置SPI”:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
資料傳輸順序
當一個位元組寫入 Tx 緩衝區時,傳輸開始。
資料位元組被載入到移位暫存器中 平行線 模式(來自內部總線)在第一位傳輸期間,之後它被傳輸到 順序的 MOSI 引腳模式,第一位或最後一位向前傳送取決於 CPI_CR1 暫存器中 LSBFIRST 位元的設定。 資料發送後設定TXE標誌 從 Tx 緩衝區到移位暫存器,並且如果 CPI_CR1 暫存器中的 TXEIE 位元被置位,也會產生中斷。

我在翻譯中突出顯示了幾個單詞,以引起人們對 STM 控制器中 SPI 實現的一項功能的關注。 在 Atmega 上,TXE 標誌(發送空,Tx 為空並準備好接收資料)僅在發送整個位元組後才設置 向外。 這裡,該標誌是在位元組插入內部移位暫存器後設定的。 由於它是與所有位元同時(並行)推送到那裡,然後順序傳輸數據,因此在位元組完全發送之前 TXE 被置位。 這很重要,因為對於我們的 LED 驅動器,我們需要在發送後拉動 LAT 引腳 所有 數據,即僅 TXE 標誌對我們來說還不夠。

這意味著我們需要另一個標誌。 讓我們來看看 25.3.7 - “狀態標誌”:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
<...>
閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
忙碌標誌
BSY 標誌由硬體設定和清除(對其寫入無效)。 BSY 標誌指示 SPI 通訊層的狀態。
它重置:
當傳輸完成時(除非在主模式下,如果傳輸是連續的)
當 SPI 被禁用時
當發生主模式錯誤時(MODF=1)
如果傳輸不連續,則 BSY 標誌在每次資料傳輸之間清除

好的,這會派上用場的。 讓我們找出 Tx 緩衝區的位置。 為此,請閱讀“SPI 資料寄存器”:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
位元 15:0 DR[15:0] 資料暫存器
接收到的資料或要傳送的資料。
資料暫存器分為兩個緩衝區 - 一個用於寫入(發送緩衝區),一個用於讀取(接收緩衝區)。 寫入資料暫存器將寫入 Tx 緩衝區,從資料暫存器讀取將傳回 Rx 緩衝區中包含的值。

好吧,還有狀態暫存器,其中有 TXE 和 BSY 標誌:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷

我們寫:

#define _SPI_DR  0x0C
#define _SPI_SR  0x08

#define BSY         0x0080
#define TXE         0x0002

void dm_shift16(uint16_t value)
{
    _SPI2_(_SPI_DR) = value; //send 2 bytes
    while (!(_SPI2_(_SPI_SR) & TXE)); //wait until they're sent
}

好吧,由於我們需要傳輸兩個位元組的 16 倍,根據 LED 驅動器輸出的數量,如下所示:

void sendLEDdata()
{
    LAT_low();
    uint8_t k = 16;
    do
    {   k--;
        dm_shift16(leds[k]);
    } while (k);

    while (_SPI2_(_SPI_SR) & BSY); // finish transmission

    LAT_pulse();
}

但我們還不知道如何拉動 LAT 引腳,所以我們將回到 I/O。

分配引腳

在STM32F1中,負責引腳狀態的暫存器非常不尋常。 很明顯,它們的數量比Atmega要多,但它們也與其他STM晶片不同。 9.1節GPIO概述:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
每個通用 I/O 端口 (GPIO) 具有兩個32位元配置暫存器(GPIOx_CRL和GPIOx_CRH)、兩個32位元資料暫存器(GPIOx_IDR和GPIOx_ODR)、一個32位元設定/重設一個(GPIOx_BSRR)、一個16位元重設暫存器(GPIOx_BRR)和一個32位元重設暫存器(GPIOx_BRR)。位元阻塞暫存器(GPIOx_LCKR)。

前兩個暫存器很不尋常,而且也很不方便,因為 16 個連接埠引腳以「每個兄弟四位」的格式分散在它們之間。 那些。 腳位 XNUMX 到 XNUMX 位於 CRL,其餘腳位位於 CRH。 同時,其餘暫存器成功包含連接埠所有引腳的位元 - 通常保留一半「保留」。

為了簡單起見,讓我們從清單的末尾開始。

我們不需要阻塞暫存器。

設定和重置暫存器非常有趣,因為它們部分地相互重複:您只能在 BSRR 中寫入所有內容,其中高 16 位元將引腳重設為零,低位將設為 1,或者您也可以使用BRR ,其低16位元僅重設引腳。 我喜歡第二個選擇。 這些暫存器很重要,因為它們提供對引腳的原子存取:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
原子設定或重置
在位元層級編程 GPIOx_ODR 時無需停用中斷:可以透過單一原子寫入操作 APB2 更改一個或多個位元。 這是透過向需要更改的位元的置位/重設暫存器(GPIOx_BSRR 或僅用於重設的 GPIOx_BRR)寫入「1」來實現的。 其他位將保持不變。

資料暫存器的名稱非常不言自明 - IDR = 輸入 方向暫存器,輸入暫存器; ODR = 產量 方向寄存器,輸出寄存器。 我們在當前專案中不需要它們。

最後,控制暫存器。 由於我們對第二個 SPI 引腳(即 PB13、PB14 和 PB15)感興趣,因此我們立即查看 CRH:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷

我們發現我們需要在 20 到 31 位元之間寫入一些內容。

上面我們已經弄清楚了我們想要從引腳中得到什麼,所以這裡我將不截圖,我只是說MODE 指定方向(如果兩個位元都設定為0,則輸入)和引腳速度(我們需要50MHz ,即兩個引腳都設定為“1”),CNF 設定模式:常規“推挽”– 00,“替代”– 10。預設情況下,如上所示,所有引腳都有倒數第三位( CNF0),它將它們設定為模式 浮動輸入.

由於我打算用這個晶片做其他事情,為了簡單起見,我為下控制暫存器和上控制暫存器定義了所有可能的 MODE 和 CNF 值。

不知怎的就像這樣

#define CNF0_0 0x00000004
#define CNF0_1 0x00000008
#define CNF1_0 0x00000040
#define CNF1_1 0x00000080
#define CNF2_0 0x00000400
#define CNF2_1 0x00000800
#define CNF3_0 0x00004000
#define CNF3_1 0x00008000
#define CNF4_0 0x00040000
#define CNF4_1 0x00080000
#define CNF5_0 0x00400000
#define CNF5_1 0x00800000
#define CNF6_0 0x04000000
#define CNF6_1 0x08000000
#define CNF7_0 0x40000000
#define CNF7_1 0x80000000
#define CNF8_0 0x00000004
#define CNF8_1 0x00000008
#define CNF9_0 0x00000040
#define CNF9_1 0x00000080
#define CNF10_0 0x00000400
#define CNF10_1 0x00000800
#define CNF11_0 0x00004000
#define CNF11_1 0x00008000
#define CNF12_0 0x00040000
#define CNF12_1 0x00080000
#define CNF13_0 0x00400000
#define CNF13_1 0x00800000
#define CNF14_0 0x04000000
#define CNF14_1 0x08000000
#define CNF15_0 0x40000000
#define CNF15_1 0x80000000

#define MODE0_0 0x00000001
#define MODE0_1 0x00000002
#define MODE1_0 0x00000010
#define MODE1_1 0x00000020
#define MODE2_0 0x00000100
#define MODE2_1 0x00000200
#define MODE3_0 0x00001000
#define MODE3_1 0x00002000
#define MODE4_0 0x00010000
#define MODE4_1 0x00020000
#define MODE5_0 0x00100000
#define MODE5_1 0x00200000
#define MODE6_0 0x01000000
#define MODE6_1 0x02000000
#define MODE7_0 0x10000000
#define MODE7_1 0x20000000
#define MODE8_0 0x00000001
#define MODE8_1 0x00000002
#define MODE9_0 0x00000010
#define MODE9_1 0x00000020
#define MODE10_0 0x00000100
#define MODE10_1 0x00000200
#define MODE11_0 0x00001000
#define MODE11_1 0x00002000
#define MODE12_0 0x00010000
#define MODE12_1 0x00020000
#define MODE13_0 0x00100000
#define MODE13_1 0x00200000
#define MODE14_0 0x01000000
#define MODE14_1 0x02000000
#define MODE15_0 0x10000000
#define MODE15_1 0x20000000

我們的引腳位於連接埠 B(基底位址 – 0x40010C00),代碼:

#define _PORTB_(mem_offset) (*(volatile uint32_t *)(0x40010C00 + (mem_offset)))

#define _BRR  0x14
#define _BSRR 0x10
#define _CRL  0x00
#define _CRH  0x04

//используем стандартный SPI2: MOSI на B15, CLK на B13
//LAT пусть будет на неиспользуемом MISO – B14

//очищаем дефолтный бит, он нам точно не нужен
_PORTB_ (_CRH) &= ~(CNF15_0 | CNF14_0 | CNF13_0 | CNF12_0);

//альтернативные функции для MOSI и SCK
_PORTB_ (_CRH) |= CNF15_1 | CNF13_1;

//50 МГц, MODE = 11
_PORTB_ (_CRH) |= MODE15_1 | MODE15_0 | MODE14_1 | MODE14_0 | MODE13_1 | MODE13_0;

並且,相應地,您可以編寫 LAT 的定義,該定義將由 BRR 和 BSRR 暫存器調整:

/*** LAT pulse – high, then low */
#define LAT_pulse() _PORTB_(_BSRR) = (1<<14); _PORTB_(_BRR) = (1<<14)

#define LAT_low() _PORTB_(_BRR) = (1<<14)

(LAT_low只是慣性,一直都是這樣,就這樣吧)

現在一切都很好,但行不通。 因為這是 STM32,所以它們可以節省電力,這意味著您需要啟用所需週邊裝置的時鐘。

打開時鐘

手錶又稱時鐘,負責計時。 我們已經注意到縮寫 RCC。 我們在文件中查找:這是重設和時鐘控制。

如上所述,幸運的是,時鐘主題中最困難的部分是由 STM 的人員為我們完成的,對此我們非常感謝他們(我將再次給出鏈接 迪哈特的網站,以明確它是多麼令人困惑)。 我們只需要負責啟用外設時脈的暫存器(外設時脈啟用暫存器)。 首先,我們找到RCC的基底位址,它位於「記憶體映射」的最開始:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷

#define _RCC_(mem_offset) (*(volatile uint32_t *)(0x40021000 + (mem_offset)))

然後單擊您嘗試在板中查找某些內容的鏈接,或者更好的是,瀏覽有關以下部分的啟用寄存器的描述 啟用暫存器。 我們可以在哪裡找到 RCC_APB1ENR 和 RCC_APB2ENR:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷

因此,它們包含 SPI2、IOPB(I/O 連接埠 B)和替代功能 (AFIO) 時脈等位元。

#define _APB2ENR 0x18
#define _APB1ENR 0x1C

#define IOPBEN 0x0008
#define SPI2EN 0x4000
#define AFIOEN 0x0001

//включаем тактирование порта B и альт. функций
_RCC_(_APB2ENR) |= IOPBEN | AFIOEN;

//включаем  тактирование SPI2
_RCC_(_APB1ENR) |= SPI2EN;

最終代碼可以找到 這裡.

如果您有機會並且希望進行測試,請像這樣連接 DM634:DAI 到 PB15,DCK 到 PB13,LAT 到 PB14。 我們用 5 伏特為驅動器供電,不要忘記連接接地線。

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷

STM8 脈寬調製

STM8上的脈寬調製

當我剛剛準備寫這篇文章時,我決定,作為一個例子,嘗試僅使用數據表來掌握不熟悉的晶片的一些功能,這樣我就不會成為一個沒有靴子的鞋匠。 STM8 非常適合這個角色:首先,我有幾個帶有 STM8S103 的中國板,其次,它不是很受歡迎,因此在互聯網上閱讀和尋找解決方案的誘惑在於缺乏這些解決方案。

該晶片還具有 數據表 и 參考手冊 RM0016,第一個是引腳分配和暫存器位址,第二個是其他所有內容。 STM8 在一個糟糕的 IDE 中用 C 語言編程 ST視覺開發.

時鐘和 I/O

預設情況下,STM8 的工作頻率為 2 MHz,必須立即修正。

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
HSI(高速內部)時鐘
HSI 時脈訊號源自於具有可程式化分頻器(16 至 1)的內部 8 MHz RC 振盪器。 它在時脈分頻暫存器(CLK_CKDIVR)中設定。
注意:開始時,選擇分頻器為 8 的 HSI RC 振盪器作為時脈訊號的主要來源。

我們在datasheet中找到暫存器位址,refman中的描述,看到該暫存器需要清零:

#define CLK_CKDIVR *(volatile uint8_t *)0x0050C6

CLK_CKDIVR &= ~(0x18);

由於我們要運行 PWM 並連接 LED,所以讓我們來看看引腳排列:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷

該晶片很小,許多功能都懸掛在同一引腳上。 方括號裡的是“替代功能”,透過“選項位元組”切換(選項位元組) – 像 Atmega 保險絲之類的東西。 您可以透過程式設計方式更改它們的值,但這不是必需的,因為新功能僅在重新啟動後才會啟動。 使用 ST Visual Programmer(與 Visual Develop 一起下載)會更容易,它可以更改這些位元組。 腳位排列顯示,第一個定時器的CH1和CH2腳隱藏在方括號內; 需要設定STVP中的AFR1和AFR0位,第二個也將第二個定時器的CH1輸出從PD4傳送到PC5。

因此,6 個引腳將控制 LED:PC6、PC7 和 PC3 用於第一個定時器,PC5、PD3 和 PA3 用於第二個定時器。

在 STM8 上設定 I/O 引腳本身比在 STM32 上更簡單、更符合邏輯:

  • 熟悉 Atmega DDR 資料方向暫存器(資料方向暫存器):1 = 輸出;
  • 第一控制暫存器CR1輸出時設定推挽模式(1)或開漏模式(0); 因為我用陰極將 LED 連接到晶片,所以我在這裡留下了 XNUMX;
  • 第二個控制暫存器 CR2 在輸出時設定時脈速度:1 = 10 MHz

#define PA_DDR     *(volatile uint8_t *)0x005002
#define PA_CR2     *(volatile uint8_t *)0x005004
#define PD_DDR     *(volatile uint8_t *)0x005011
#define PD_CR2     *(volatile uint8_t *)0x005013
#define PC_DDR     *(volatile uint8_t *)0x00500C
#define PC_CR2     *(volatile uint8_t *)0x00500E

PA_DDR = (1<<3); //output
PA_CR2 |= (1<<3); //fast
PD_DDR = (1<<3); //output
PD_CR2 |= (1<<3); //fast
PC_DDR = ((1<<3) | (1<<5) | (1<<6) | (1<<7)); //output
PC_CR2 |= ((1<<3) | (1<<5) | (1<<6) | (1<<7)); //fast

脈寬調變設定

首先,讓我們定義術語:

  • PWM頻率 – 計時器計時的頻率;
  • 自動裝填、AR – 定時器計數的可自動載入值(脈衝週期);
  • 更新事件,UEV – 當計時器計數到 AR 時發生的事件;
  • 脈寬調變佔空比 – PWM 佔空比,通常稱為「佔空因數」;
  • 捕獲/比較值 – 定時器已計數的捕獲/比較值 會做某事 (在 PWM 的情況下,它反轉輸出訊號);
  • 預載值 – 預載值。 比較值 當定時器計時時不能改變,否則 PWM 週期將中斷。 因此,新傳輸的值被放置在緩衝區中,並在計時器到達倒數結束時被拉出並重置;
  • 邊緣對齊 и 中心對齊模式 – 沿著邊界和中心對齊,與 Atmel 的相同 快速脈寬調製 и 相位校正 PWM.
  • OCiREF,輸出比較參考訊號 – 參考輸出訊號,實際上是 PWM 模式下對應腳位上出現的訊號。

從引腳排列中可以清楚地看出,兩個定時器具有 PWM 功能——第一個和第二個。 兩者都是 16 位,第一個有很多附加功能(特別是它可以向上和向下計數)。 我們需要兩者平等地工作,所以我決定從明顯較差的第二個開始,以免意外使用不存在的東西。 一些問題是,參考手冊中所有定時器的 PWM 功能的描述都在有關第一個定時器(17.5.7 PWM 模式)的章節中,因此您必須始終在整個文件中來回跳轉。

STM8上的PWM比Atmega上的PWM有一個重要的優勢:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
邊界對齊 PWM
帳戶配置從下到上
如果 TIM_CR1 暫存器中的 DIR 位元清零,則自下而上計數有效
例子
此範例使用第一種 PWM 模式。 只要 TIM1_CNT < TIM1_CCRi,PWM 參考訊號 OCiREF 就會保持高電位。 否則需要低水平。 如果 TIM1_CCRi 暫存器中的比較值大於自動載入值(TIM1_ARR 暫存器),則 OCiREF 訊號保持為 1。 如果比較值為 0,則 OCiREF 保持為零。...

STM8定時器期間 更新事件 首先檢查 比較值,然後才產生參考訊號。 Atmega 的計時器首先出錯,然後進行比較,結果是 compare value == 0 輸出是一根針,必須以某種方式處理它(例如,以程式方式反轉邏輯)。

那我們想要做的是:8位元PWM(AR == 255),從下往上數,沿著邊框對齊。 由於燈泡透過陰極連接到晶片,因此 PWM 應輸出 0(LED 亮起),直到 比較值 和 1 之後。

我們已經讀過一些 PWM模式,因此我們透過在參考手冊中搜尋這句話(18.6.8 - TIMx_CCMR1)來找到第二個定時器所需的暫存器:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
110:第一個 PWM 模式-從下到上計數時,當 TIMx_CNT < TIMx_CCR1 時,第一個通道有效。 否則,第一通道處於非活動狀態。 [文件中還有來自定時器 1 的錯誤複製貼上] 111:第二個 PWM 模式 – 從下到上計數時,當 TIMx_CNT < TIMx_CCR1 時,第一個通道處於非活動狀態。 否則,第一個通道處於活動狀態。

由於 LED 透過陰極連接到 MK,因此第二種模式適合我們(第一種模式也是如此,但我們還不知道)。

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
位元 3 OC1PE:啟用引腳 1 預載
0:TIMx_CCR1 上的預載暫存器被禁用。 您可以隨時寫入 TIMx_CCR1。 新值立即生效。
1:TIMx_CCR1 上的預載暫存器啟用。 讀/寫操作存取預載暫存器。 預先載入值 TIMx_CCR1 在每次更新事件期間載入到影子暫存器。
*註:為了使 PWM 模式正常運作,必須使能預載暫存器。 這在單一訊號模式下是不必要的(OPM 位元在 TIMx_CR1 暫存器中設定)。

好的,讓我們打開第二個計時器的三個通道所需的一切:

#define TIM2_CCMR1 *(volatile uint8_t *)0x005307
#define TIM2_CCMR2 *(volatile uint8_t *)0x005308
#define TIM2_CCMR3 *(volatile uint8_t *)0x005309

#define PWM_MODE2   0x70 //PWM mode 2, 0b01110000
#define OCxPE       0x08 //preload enable

TIM2_CCMR1 = (PWM_MODE2 | OCxPE);
TIM2_CCMR2 = (PWM_MODE2 | OCxPE);
TIM2_CCMR3 = (PWM_MODE2 | OCxPE);

AR由兩個八位元寄存器組成,一切都很簡單:

#define TIM2_ARRH  *(volatile uint8_t *)0x00530F
#define TIM2_ARRL  *(volatile uint8_t *)0x005310

TIM2_ARRH = 0;
TIM2_ARRL = 255;

第二個定時器只能從下往上計數,沿著邊框對齊,不需要做任何改變。 讓我們將分頻器設定為 256。對於第二個定時器,分頻器在 TIM2_PSCR 暫存器中設置,並且是 XNUMX 的冪:

#define TIM2_PSCR  *(volatile uint8_t *)0x00530E

TIM2_PSCR = 8;

剩下的就是打開結論和第二個計時器本身。 第一個問題透過暫存器解決 捕獲/比較 啟用:有兩三個通道不對稱地分佈在其中。 在這裡我們也可以了解到可以改變訊號的極性,即原則上,可以使用 PWM 模式 1。我們寫道:

#define TIM2_CCER1 *(volatile uint8_t *)0x00530A
#define TIM2_CCER2 *(volatile uint8_t *)0x00530B

#define CC1E  (1<<0) // CCER1
#define CC2E  (1<<4) // CCER1
#define CC3E  (1<<0) // CCER2

TIM2_CCER1 = (CC1E | CC2E);
TIM2_CCER2 = CC3E;

最後,我們啟動 TIMx_CR1 暫存器中的定時器:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷

#define TIM2_CR1   *(volatile uint8_t *)0x005300

TIM2_CR1 |= 1;

讓我們來寫一個 AnalogWrite() 的簡單模擬,它將實際值傳送到計時器進行比較。 暫存器的命名是可預測的 捕獲/比較暫存器,每個通道有兩個:TIM8_CCRxL 中的低位元 2 位元和 TIM2_CCRxH 中的高位元。 由於我們建立了 8 位元 PWM,因此僅寫入最低有效位元就足夠了:

#define TIM2_CCR1L *(volatile uint8_t *)0x005312
#define TIM2_CCR2L *(volatile uint8_t *)0x005314
#define TIM2_CCR3L *(volatile uint8_t *)0x005316

void setRGBled(uint8_t r, uint8_t g, uint8_t b)
{
    TIM2_CCR1L = r;
    TIM2_CCR2L = g;
    TIM2_CCR3L = b;
}

細心的讀者會注意到,我們的 PWM 略有缺陷,無法產生 100% 填充(最大值為 255,訊號在定時器週期中反轉)。 對於 LED 來說,這並不重要,細心的讀者已經可以猜到如何解決它。

第二個定時器上的 PWM 運作正常,讓我們繼續討論第一個計時器。

第一個定時器在相同的暫存器中具有完全相同的位元(只是第二個定時器中保留「保留」的那些位元在第一個定時器中積極用於各種高級功能)。 因此,在資料表中找到相同暫存器的位址並複製程式碼就足夠了。 好吧,改變分頻器的值,因為...... 第一個定時器想要接收的不是 16 的冪,而是兩個暫存器中精確的 XNUMX 位元值 預分頻器高 и 。 我們做了一切,但...第一個計時器不起作用。 怎麼了?

這個問題只能透過查看關於定時器1的控制暫存器的整個部分來解決,我們在其中尋找第二個定時器沒有的控制暫存器。 將有 17.7.30 中斷暫存器(TIM1_BKR),其中有這一點:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
啟用主輸出

#define TIM1_BKR   *(volatile uint8_t *)0x00526D

TIM1_BKR = (1<<7);

現在一切都確定了,程式碼 那裡.

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷

STM8 復用

STM8 上的複用

第三個迷你項目是將八個RGB LED以PWM模式連接到第二個定時器,並使它們顯示不同的顏色。 它基於 LED 多路復用的概念,即如果您非常非常快速地打開和關閉 LED,我們會覺得它們一直處於開啟狀態(視覺的持久性,視覺感知慣性)。 我曾經做過 Arduino 上是這樣的.

工作演算法如下所示:

  • 連接第一個RGB LED的陽極;
  • 點燃它,向陰極發送必要的信號;
  • 等到PWM週期結束;
  • 連接第二個RGB LED的陽極;
  • 點燃它...

嗯,等等。 當然,為了實現良好的操作,需要連接陽極並同時「點亮」LED。 嗯,或者說差不多了。 無論如何,我們需要編寫一段程式碼,在第二個計時器的三個通道中輸出值,在達到 UEV 時更改它們,同時更改當前活動的 RGB LED。

由於 LED 切換是自動的,因此我們需要建立一個“視訊記憶體”,中斷處理程序將從中接收資料。 這是一個簡單的陣列:

uint8_t colors[8][3];

為了改變特定 LED 的顏色,只需將所需的值寫入該陣列即可。 該變數將負責啟動 LED 的數量

uint8_t cnt;

解復用器

奇怪的是,為了實現正確的多重化,我們需要一個 CD74HC238 解復用器。 解復用器 - 在硬體中實現運算符的晶片 <<。 透過三個輸入引腳(位元 0、1 和 2),我們向其提供一個三位數字 X,作為回應,它會啟動輸出數字(1<<X)。 晶片的其餘輸入用於擴展整個設計。 我們需要這個晶片不僅是為了減少微控制器佔用的接腳數量,也是為了安全——以免意外打開過多的LED,也不會燒掉MK。 該晶片售價一美分,應始終存放在您的家庭藥櫃中。

我們的 CD74HC238 將負責向所需 LED 的陽極提供電壓。 在成熟的多路復用中,它將透過 P-MOSFET 向列提供電壓,但在本演示中可以直接提供,因為它消耗 20 mA 的電流,根據 絕對最大額定值 在數據表中。 從 資料表 CD74HC238 我們需要引腳排列和這個備忘單:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
H = 高電壓電平,L = 低電壓電平,X – 無關

我們將E2和E1接地,將E3、A0、A1和A3連接到STM5的接腳PD3、PC4、PC5和PC8。 由於上表包含低電平和高電平,因此我們將這些引腳配置為推挽引腳。

脈寬調製

第二個定時器上的 PWM 的配置方式與前面的故事相同,但有兩點不同:

首先,我們需要開啟中斷 更新事件 (UEV),它將呼叫一個切換活動 LED 的函數。 這是透過改變位子來完成的 更新中斷使能 在一個有明顯名字的登記冊中

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
中斷使能暫存器

#define TIM2_IER   *(volatile uint8_t *)0x005303

//enable interrupt
TIM2_IER = 1;

第二個差異與復用現像有關,例如 重影 – 二極體的寄生輝光。 在我們的例子中,出現這種情況的原因可能是定時器在 UEV 上引起了中斷,並繼續計時,並且中斷處理程序沒有時間在定時器開始向引腳寫入內容之前切換 LED。 為了解決這個問題,您必須反轉邏輯(0 = 最大亮度,255 = 沒有亮起)並避免極端的佔空比值。 那些。 確保 UEV 之後 LED 在一個 PWM 週期內完全熄滅。

改變極性:

//set polarity 
    TIM2_CCER1 |= (CC1P | CC2P);
    TIM2_CCER2 |= CC3P;

避免將 r、g 和 b 設定為 255,並記住在使用它們時反轉它們。

中斷

中斷的本質是在某種情況下晶片停止執行主程式並呼叫一些外部函數。 中斷是由於外部或內部影響(包括定時器)而發生的。

當我們第一次在ST Visual Develop中建立一個專案時,除了 main.c 我們收到一個帶有神秘文件的窗口 stm8_interrupt_vector.c,自動包含在項目中。 在這個檔案中,為每個中斷分配了一個函數 NonHandledInterrupt。 我們需要將我們的函數綁定到所需的中斷。

資料表中有一個中斷向量表,我們可以在其中找到我們需要的中斷向量:

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷
13 TIM2 更新/溢出
14 TIM2 捕獲/比較

我們需要更改 UEV 處的 LED,因此需要中斷#13。

因此,首先在文件中 stm8_interrupt_vector.c 將負責 13 號中斷 (IRQ13) 的函數的預設名稱變更為您自己的:

{0x82, TIM2_Overflow}, /* irq13 */

其次,我們必須建立一個文件 main.h 包含以下內容:

#ifndef __MAIN_H
#define __MAIN_H

@far @interrupt void TIM2_Overflow (void);
#endif

最後,將此函數寫入您的 main.c:

@far @interrupt void TIM2_Overflow (void)
{
    PD_ODR &= ~(1<<5); // вырубаем демультиплексор
    PC_ODR = (cnt<<3); // записываем в демультиплексор новое значение
    PD_ODR |= (1<<5); // включаем демультиплексор

    TIM2_SR1 = 0; // сбрасываем флаг Update Interrupt Pending

    cnt++; 
    cnt &= 7; // двигаем счетчик LED

    TIM2_CCR1L = ~colors[cnt][0]; // передаем в буфер инвертированные значения
    TIM2_CCR2L = ~colors[cnt][1]; // для следующего цикла ШИМ
    TIM2_CCR3L = ~colors[cnt][2]; // 

    return;
}

剩下的就是啟用中斷。 這是使用彙編命令完成的 rim - 你必須在以下位置尋找它 編程手冊:

//enable interrupts
_asm("rim");

另一個彙編命令是 sim – 關閉中斷。 在將新值寫入「視訊記憶體」時必須關閉它們,以便在錯誤時刻引起的中斷不會破壞陣列。

所有代碼 - 在 GitHub 上.

閱讀資料手冊2:STM32上的SPI; STM8 上的 PWM、定時器和中斷

如果至少有人覺得這篇文章有用,那我就沒有白寫。 我很高興收到評論和評論,我會盡力回答所有問題。

來源: www.habr.com

添加評論