Ini adalah artikel pertama saya tentang Habré, jadi saya meminta Anda untuk tidak membuang benda berat. Terima kasih sebelumnya.
Mari kita mulai dengan background. Suatu ketika saya harus beralih ke mikrokontroler ST ARM. Hal ini disebabkan oleh fakta bahwa PIC dan AVR sudah tidak ada lagi dan menginginkan petualangan baru. Dari yang tersedia di toko roti dan sejumlah besar artikel tentang "mulai cepat", pilihan jatuh pada STM32F100.
Saya sudah terbiasa bekerja di IAR. Ya, ada IDE lain, tetapi fitur IAR sudah cukup bagi saya: editor yang relatif nyaman, bukan debugger yang buruk, dan cukup nyaman untuk bekerja dengan register selama debugging.
Ketika saya mencoba membuat proyek pertama saya kecewa - CMSIS! Orang lain, tetapi bagi saya itu (dan tetap) horor: banyak beech, struktur panjang dan tidak bisa dimengerti bagi saya. Tidak menarik untuk mempelajari semua ini. Saya mencoba mengumpulkan beberapa contoh dan menyadari bahwa ini bukan metode kami.
Apakah tidak ada pilihan lain? Ada. Yang dibangun ke dalam IAR: iostm32f10xx4.h dan sejenisnya termasuk. Tidak buruk sama sekali:
RCC_APB2ENR_bit.ADC1EN = 1; // ADC
Yang tersisa hanyalah memasukkannya ke dalam kelas dan menggunakannya. Dan dia melakukannya. Setelah beberapa waktu, dibutuhkan untuk membuat kode untuk STM32f4xx. Dan di sini lagi penyergapan - tidak ada orang yang ikut serta. Apa yang harus dilakukan? - tulis sendiri. Saya menganalisis perpustakaan yang ditulis sendiri dan memutuskan untuk melakukan sedikit berbeda. Ceritanya akan seperti ini.
Mulailah
Saya tidak akan berbicara tentang menginstal IAR dan driver untuk debugger. Tidak ada hal baru disini. Saya memiliki IAR 8 dengan batas kode 32kB. Pengontrol STM32F103 yang dipasang pada papan pil plue dipilih untuk pengoperasian.
Luncurkan IAR, buat proyek c ++, pilih pengontrol yang diinginkan.
Langkah selanjutnya adalah mempelajari dokumentasi. Kami akan tertarik dengan manual Referensi RM0008. Yang utama adalah membaca dengan cermat.
Secara umum, ketika saya mengajari pekerja saya tentang pengontrol program, saya memberi tugas - untuk menyalakan LED (terhubung ke kaki pengontrol), menggunakan debugger, mengedit register dan membaca dokumentasi.
Modul RCC. Selipkan
Modul ini biasanya dilupakan. Mereka hanya mengingat ketika LED tidak mungkin berkedip.
Ingat! Untuk menyalakan periferal apa pun, Anda perlu menerapkan pulsa jam ke sana! Anda tidak bisa hidup tanpanya.
Port I / O berada di bus APB2. Kami menemukan dalam dokumentasi register untuk mengontrol clocking bus ini, ini adalah RCC_APB2ENR:
Untuk mengaktifkan clocking port C (LED hanya disolder ke PC13), Anda perlu menuliskannya ke bit IOPCEN.
Sekarang kita akan menemukan alamat register RCC_APB2ENR. Offsetnya adalah 0x18, alamat dasar untuk register RCC adalah 0x40021000.
Untuk membuatnya nyaman bekerja dengan bit, mari buat struktur:
typedef struct
{
uint32_t AFIOEN : 1;
uint32_t : 1;
uint32_t IOPAEN : 1;
uint32_t IOPBEN : 1;
uint32_t IOPCEN : 1;
uint32_t IOPDEN : 1;
uint32_t IOPEEN : 1;
uint32_t : 2;
uint32_t ADC1EN : 1;
uint32_t ADC2EN : 1;
uint32_t TIM1EN : 1;
uint32_t SPI1EN : 1;
uint32_t : 1;
uint32_t USART1EN : 1;
uint32_t :17;
} RCC_APB2ENR_b;
Agar tidak menderita nanti, kami akan segera mendaftar semua alamat register:
enum AddrRCC
{
RCC_CR = 0x40021000,
RCC_CFGR = 0x40021004,
RCC_CIR = 0x40021008,
RCC_APB2RSTR = 0x4002100C,
RCC_APB1RSTR = 0x40021010,
RCC_AHBENR = 0x40021014,
RCC_APB2ENR = 0x40021018,
RCC_APB1ENR = 0x4002101C,
RCC_BDCR = 0x40021020,
RCC_CSR = 0x40021024
};
sekarang tinggal menulis kode untuk mengaktifkan periferal:
static void EnablePort(uint8_t port_name)
{
volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
switch (port_name)
{
case 'A': apb2enr->IOPAEN = 1; break;
case 'a': apb2enr->IOPAEN = 1; break;
case 'B': apb2enr->IOPBEN = 1; break;
case 'b': apb2enr->IOPBEN = 1; break;
case 'C': apb2enr->IOPCEN = 1; break;
case 'c': apb2enr->IOPCEN = 1; break;
case 'D': apb2enr->IOPDEN = 1; break;
case 'd': apb2enr->IOPDEN = 1; break;
case 'E': apb2enr->IOPEEN = 1; break;
case 'e': apb2enr->IOPEEN = 1; break;
}
}
Saat bekerja dengan register, jangan lupa tentang volatile , jika tidak, setelah optimasi oleh compiler, kami akan mencari kesalahan dalam waktu lama dan memarahi pengembang compiler.
Kami melakukan hal yang sama untuk mengaktifkan pencatatan jam kerja periferal lain.
Hasilnya, kami mendapat kelas berikut (tidak semuanya terdaftar):
STM32F1xx_RCC.h
#pragma once
#include "stdint.h"
namespace STM32F1xx
{
class RCC
{
protected:
enum AddrRCC
{
RCC_CR = 0x40021000,
RCC_CFGR = 0x40021004,
RCC_CIR = 0x40021008,
RCC_APB2RSTR = 0x4002100C,
RCC_APB1RSTR = 0x40021010,
RCC_AHBENR = 0x40021014,
RCC_APB2ENR = 0x40021018,
RCC_APB1ENR = 0x4002101C,
RCC_BDCR = 0x40021020,
RCC_CSR = 0x40021024
};
typedef struct {
uint32_t HSION : 1;
uint32_t HSIRDY : 1;
uint32_t : 1;
uint32_t HSI_TRIM : 5;
uint32_t HSI_CAL : 8;
uint32_t HSEON : 1;
uint32_t HSERDY : 1;
uint32_t HSEBYP : 1;
uint32_t CSSON : 1;
uint32_t : 4;
uint32_t PLLON : 1;
uint32_t PLLRDY : 1;
uint32_t : 6;
} RCC_CR_b;
typedef struct {
uint32_t SW : 2;
uint32_t SWS : 2;
uint32_t HPRE : 4;
uint32_t PPRE1 : 3;
uint32_t PPRE2 : 3;
uint32_t ADC_PRE : 2;
uint32_t PLLSRC : 1;
uint32_t PLLXTPRE : 1;
uint32_t PLLMUL : 4;
uint32_t USBPRE : 1;
uint32_t : 1;
uint32_t MCO : 3;
uint32_t : 5;
} RCC_CFGR_b;
typedef struct
{
uint32_t TIM2EN : 1;
uint32_t TIM3EN : 1;
uint32_t TIM4EN : 1;
uint32_t : 8;
uint32_t WWDGEN : 1;
uint32_t : 2;
uint32_t SPI2EN : 1;
uint32_t : 2;
uint32_t USART2EN : 1;
uint32_t USART3EN : 1;
uint32_t : 2;
uint32_t I2C1EN : 1;
uint32_t I2C2EN : 1;
uint32_t USBEN : 1;
uint32_t : 1;
uint32_t CANEN : 1;
uint32_t : 1;
uint32_t BKPEN : 1;
uint32_t PWREN : 1;
uint32_t : 3;
} RCC_APB1ENR_b;
typedef struct
{
uint32_t AFIOEN : 1;
uint32_t : 1;
uint32_t IOPAEN : 1;
uint32_t IOPBEN : 1;
uint32_t IOPCEN : 1;
uint32_t IOPDEN : 1;
uint32_t IOPEEN : 1;
uint32_t : 2;
uint32_t ADC1EN : 1;
uint32_t ADC2EN : 1;
uint32_t TIM1EN : 1;
uint32_t SPI1EN : 1;
uint32_t : 1;
uint32_t USART1EN : 1;
uint32_t :17;
} RCC_APB2ENR_b;
typedef struct {
uint32_t DMAEN : 1;
uint32_t : 1;
uint32_t SRAMEN : 1;
uint32_t : 1;
uint32_t FLITFEN : 1;
uint32_t : 1;
uint32_t CRCEN : 1;
uint32_t :25;
} RCC_AHBENR_r;
public:
static void EnablePort(uint8_t port_name)
{
volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
switch (port_name)
{
case 'A': apb2enr->IOPAEN = 1; break;
case 'a': apb2enr->IOPAEN = 1; break;
case 'B': apb2enr->IOPBEN = 1; break;
case 'b': apb2enr->IOPBEN = 1; break;
case 'C': apb2enr->IOPCEN = 1; break;
case 'c': apb2enr->IOPCEN = 1; break;
case 'D': apb2enr->IOPDEN = 1; break;
case 'd': apb2enr->IOPDEN = 1; break;
case 'E': apb2enr->IOPEEN = 1; break;
case 'e': apb2enr->IOPEEN = 1; break;
}
}
static void DisablePort(char port_name)
{
volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
switch (port_name)
{
case 'A': apb2enr->IOPAEN = 0; break;
case 'a': apb2enr->IOPAEN = 0; break;
case 'B': apb2enr->IOPBEN = 0; break;
case 'b': apb2enr->IOPBEN = 0; break;
case 'C': apb2enr->IOPCEN = 0; break;
case 'c': apb2enr->IOPCEN = 0; break;
case 'D': apb2enr->IOPDEN = 0; break;
case 'd': apb2enr->IOPDEN = 0; break;
case 'E': apb2enr->IOPEEN = 0; break;
case 'e': apb2enr->IOPEEN = 0; break;
}
}
static void EnableAFIO()
{
volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
apb2enr->AFIOEN = 1;
}
static void DisableAFIO()
{
volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
apb2enr->AFIOEN = 0;
}
static void EnableI2C(int PortNumber)
{
switch (PortNumber)
{
case 1:
{
volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);
apb1enr->I2C1EN = 1;
break;
}
case 2:
{
volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);
apb1enr->I2C2EN = 1;
break;
}
}
}
static void EnableUART(int PortNumber)
{
switch (PortNumber)
{
case 1:
{
volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
apb2enr->USART1EN = 1;
break;
}
case 2:
{
volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);
apb1enr->USART2EN = 1;
break;
}
case 3:
{
volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);
apb1enr->USART3EN = 1;
break;
}
}
}
static void DisableUART(int PortNumber)
{
switch (PortNumber)
{
case 1:
{
volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
apb2enr->USART1EN = 0;
break;
}
case 2:
{
volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);
apb1enr->USART2EN = 0;
break;
}
case 3:
{
volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);
apb1enr->USART3EN = 0;
break;
}
}
}
static void EnableSPI(int PortNumber)
{
switch (PortNumber)
{
case 1:
{
volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
apb2enr->SPI1EN = 1;
break;
}
case 2:
{
volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);
apb1enr->SPI2EN = 1;
break;
}
}
}
static void DisableSPI(int PortNumber)
{
switch (PortNumber)
{
case 1:
{
volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
apb2enr->SPI1EN = 0;
break;
}
case 2:
{
volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);
apb1enr->SPI2EN = 0;
break;
}
}
}
static void EnableDMA()
{
volatile RCC_AHBENR_r* ahbenr = reinterpret_cast<RCC_AHBENR_r*>(RCC_AHBENR);
ahbenr->DMAEN = 1;
}
static void DisableDMA()
{
volatile RCC_AHBENR_r* ahbenr = reinterpret_cast<RCC_AHBENR_r*>(RCC_AHBENR);
ahbenr->DMAEN = 0;
}
};
}
Sekarang Anda dapat melampirkan file di main.cpp dan menggunakan:
#include "STM32F1xx_RCC.h"
using namespace STM32F1xx;
int main()
{
RCC::EnablePort('c');
return 0;
}
Sekarang Anda dapat bekerja dengan port. GPIO
Buka bagian I / O untuk tujuan umum dan fungsi alternatif dalam dokumentasi. Temukan tabel konfigurasi bit Port: Bit
CNF [1: 0] mengatur mode operasi port (input analog, input digital, output), bit MODE [1: 0] sesuai dengan kecepatan operasi port dalam mode output.
Mari kita lihat register GPIOx_CRL dan GPIOx_CRH (x = A, B, C, ...), Anda
dapat melihat bahwa bit berjalan secara berurutan:
CNF [1: 0], MODE [1: 0]
lalu buat konstanta dengan mode port
enum mode_e
{
ANALOGINPUT = 0,
INPUT = 4,
INPUTPULLED = 8,
OUTPUT_10MHZ = 1,
OUTPUT_OD_10MHZ = 5,
ALT_OUTPUT_10MHZ = 9,
ALT_OUTPUT_OD_10MHZ = 13,
OUTPUT_50MHZ = 3,
OUTPUT_OD_50MHZ = 7,
ALT_OUTPUT_50MHZ = 11,
ALT_OUTPUT_OD_50MHZ = 15,
OUTPUT_2MHZ = 2,
OUTPUT_OD_2MHZ = 6,
ALT_OUTPUT_2MHZ = 10,
ALT_OUTPUT_OD_2MHZ = 14,
OUTPUT = 3,
OUTPUT_OD = 7,
ALT_OUTPUT = 11,
ALT_OUTPUT_OD = 15
};
maka metode konfigurasi akan terlihat seperti ini:
// pin_number -
void Mode(mode_e mode)
{
uint32_t* addr;
if(pin_number > 7)
addr = reinterpret_cast<uint32_t*>(GPIOA_CRH);
else
addr = reinterpret_cast<uint32_t*>(GPIOA_CRL);
int bit_offset;
if(pin_number > 7)
bit_offset = (pin_number - 8) * 4;
else
bit_offset = pin_number * 4;
uint32_t mask = ~(15 << bit_offset);
*addr &= mask;
*addr |= ((int)mode) << bit_offset;
}
sekarang kita dapat membuat metode yang lebih nyaman untuk memilih mode:
void ModeInput() { Mode(INPUT); }
void ModeAnalogInput() { Mode(ANALOGINPUT); }
void ModeInputPulled() { Mode(INPUTPULLED); }
void ModeOutput() { Mode(OUTPUT); }
void ModeOutputOpenDrain() { Mode(OUTPUT_OD); }
void ModeAlternate() { Mode(ALT_OUTPUT); }
void ModeAlternateOpenDrain() { Mode(ALT_OUTPUT_OD); }
Dalam dokumentasi, kami menemukan alamat register kontrol untuk port dan mencantumkannya:
enum AddrGPIO
{
PortA = 0x40010800,
GPIOA_CRL = 0x40010800,
GPIOA_CRH = 0x40010804,
GPIOA_IDR = 0x40010808,
GPIOA_ODR = 0x4001080C,
GPIOA_BSRR = 0x40010810,
GPIOA_BRR = 0x40010814,
GPIOA_LCKR = 0x40010818,
PortB = 0x40010C00,
PortC = 0x40011000,
PortD = 0x40011400,
PortE = 0x40011800,
PortF = 0x40011C00,
PortG = 0x40012000
};
Berpikir lama untuk menggunakan alamat dasar dan offset atau alamat absolut. Pada akhirnya, saya berhenti di yang terakhir. Ini menambahkan beberapa overhead, tetapi lebih mudah untuk menemukannya di memori selama debugging.
Mari kita modernisasi metode ini:
if(pin_number > 7)
addr = reinterpret_cast<uint32_t*>(GPIOA_CRH - PortA + PortAddr);
else
addr = reinterpret_cast<uint32_t*>(GPIOA_CRL - PortA + PortAddr);
Mungkin seseorang akan mengalami kedutan mata, tetapi saya belum menemukan yang lebih cantik.
Untuk mentransfer kaki ke keadaan logis yang diinginkan, itu cukup untuk menulis bit yang sesuai di register ODRx. Misalnya seperti ini:
void Set(bool st)
{
uint32_t* addr;
addr = reinterpret_cast<uint32_t*>(GPIOA_ODR - PortA + PortAddr);
if(st)
*addr |= 1 << pin_number;
else
{
int mask = ~(1 << pin_number);
*addr &= mask;
}
}
Anda juga dapat menggunakan register GPIOx_BSRR untuk mengontrol status.
Dengan analogi kita membuat metode untuk membaca status port, metode konfigurasi dan inisialisasi (jangan lupa aktifkan clocking). Hasilnya, kami mendapatkan kelas berikut untuk bekerja dengan port:
STM32F1xx_Pin.h
#pragma once
#include <stdint.h>
#include "STM32F1xx_RCC.h"
namespace STM32F1xx
{
class Pin
{
public:
enum mode_e
{
ANALOGINPUT = 0,
INPUT = 4,
INPUTPULLED = 8,
OUTPUT_10MHZ = 1,
OUTPUT_OD_10MHZ = 5,
ALT_OUTPUT_10MHZ = 9,
ALT_OUTPUT_OD_10MHZ = 13,
OUTPUT_50MHZ = 3,
OUTPUT_OD_50MHZ = 7,
ALT_OUTPUT_50MHZ = 11,
ALT_OUTPUT_OD_50MHZ = 15,
OUTPUT_2MHZ = 2,
OUTPUT_OD_2MHZ = 6,
ALT_OUTPUT_2MHZ = 10,
ALT_OUTPUT_OD_2MHZ = 14,
OUTPUT = 3,
OUTPUT_OD = 7,
ALT_OUTPUT = 11,
ALT_OUTPUT_OD = 15
};
private:
enum AddrGPIO
{
PortA = 0x40010800,
GPIOA_CRL = 0x40010800,
GPIOA_CRH = 0x40010804,
GPIOA_IDR = 0x40010808,
GPIOA_ODR = 0x4001080C,
GPIOA_BSRR = 0x40010810,
GPIOA_BRR = 0x40010814,
GPIOA_LCKR = 0x40010818,
PortB = 0x40010C00,
PortC = 0x40011000,
PortD = 0x40011400,
PortE = 0x40011800,
PortF = 0x40011C00,
PortG = 0x40012000
};
private:
int pin_number;
int PortAddr;
public:
Pin() { }
Pin(char port_name, int pin_number) { Init(port_name, pin_number); }
~Pin()
{
Off();
ModeAnalogInput();
}
public:
void Init(char port_name, int pin_number)
{
this->pin_number = pin_number;
RCC::EnablePort(port_name);
switch (port_name)
{
case 'A': PortAddr = PortA; break;
case 'a': PortAddr = PortA; break;
case 'B': PortAddr = PortB; break;
case 'b': PortAddr = PortB; break;
case 'C': PortAddr = PortC; break;
case 'c': PortAddr = PortC; break;
case 'D': PortAddr = PortD; break;
case 'd': PortAddr = PortD; break;
case 'E': PortAddr = PortE; break;
case 'e': PortAddr = PortE; break;
}
}
void ModeInput() { Mode(INPUT); }
void ModeAnalogInput() { Mode(ANALOGINPUT); }
void ModeInputPulled() { Mode(INPUTPULLED); }
void ModeOutput() { Mode(OUTPUT); }
void ModeOutputOpenDrain() { Mode(OUTPUT_OD); }
void ModeAlternate() { Mode(ALT_OUTPUT); }
void ModeAlternateOpenDrain() { Mode(ALT_OUTPUT_OD); }
void NoPullUpDown()
{
uint32_t* addr;
if(pin_number > 7)
addr = reinterpret_cast<uint32_t*>(GPIOA_CRH - PortA + PortAddr);
else
addr = reinterpret_cast<uint32_t*>(GPIOA_CRL - PortA + PortAddr);
int bit_offset;
if(pin_number > 7)
bit_offset = (pin_number - 8) * 4;
else
bit_offset = pin_number * 4;
int mask = ~((1 << 3) << bit_offset);
*addr &= mask;
}
void Mode(mode_e mode)
{
uint32_t* addr;
if(pin_number > 7)
addr = reinterpret_cast<uint32_t*>(GPIOA_CRH - PortA + PortAddr);
else
addr = reinterpret_cast<uint32_t*>(GPIOA_CRL - PortA + PortAddr);
int bit_offset;
if(pin_number > 7)
bit_offset = (pin_number - 8) * 4;
else
bit_offset = pin_number * 4;
uint32_t mask = ~(15 << bit_offset);
*addr &= mask;
*addr |= ((int)mode) << bit_offset;
}
void Set(bool st)
{
uint32_t* addr;
addr = reinterpret_cast<uint32_t*>(GPIOA_ODR - PortA + PortAddr);
if(st)
*addr |= 1 << pin_number;
else
{
int mask = ~(1 << pin_number);
*addr &= mask;
}
}
void On()
{
uint32_t* addr;
addr = reinterpret_cast<uint32_t*>(GPIOA_ODR - PortA + PortAddr);
int bit_offset = pin_number;
*addr |= 1 << bit_offset;
}
void Off()
{
uint32_t* addr;
addr = reinterpret_cast<uint32_t*>(GPIOA_ODR - PortA + PortAddr);
int bit_offset = pin_number;
int mask = ~(1 << bit_offset);
*addr &= mask;
}
bool Get()
{
uint32_t* addr = reinterpret_cast<uint32_t*>(GPIOA_IDR - PortA + PortAddr);
int bit_offset = pin_number;
int mask = (1 << bit_offset);
bool ret_val = (*addr & mask);
return ret_val;
}
};
};
Baiklah, mari kita coba:
#include "STM32F1xx_Pin.h"
using namespace STM32F1xx;
Pin led('c', 13);
int main()
{
led.ModeOutput();
led.On();
led.Off();
return 0;
}
Kami pergi melalui debugger dan memastikan bahwa LED pertama menyala (setelah led.ModeOutput ();), kemudian padam (led.On ();) dan menyala lagi (led.Off ();). Ini karena LED dihubungkan ke kaki melalui saluran listrik. Karena itu, saat pin rendah, LED menyala.
Bukan total yang bagus
Dalam artikel ini, saya mencoba (semoga berhasil) untuk menunjukkan bagaimana Anda dapat sedikit menyederhanakan hidup Anda, membuat kode lebih mudah dibaca. Atau sebaliknya - bagaimana tidak melakukannya. Setiap orang akan memutuskan sendiri.
Dimungkinkan untuk hanya menulis pembungkus untuk CMSIS, tetapi ini tidak menarik.
Terima kasih atas waktu Anda. Jika Anda tertarik dengan sekuelnya, beri tahu saya.