Blue Pill (STM32F103C8) + Rust: ADC Scan Mode dengan Discontinuous Conversion
Pada artikel sebelumnya kita sudah membahas mengenai scan mode ADC pada Blue Pill (STM32F103C8) mulai dari pengenalan scan mode hingga contoh program menggunakan Rust. Pada scan mode ADC STM32F103C8 kita hanya bisa menentukan satu grup channel yang akan dikonversi dengan sekali perintah dari CPU. Lalu bagaimana jika kita ingin mengonversi beberapa grup channel? Disinilah peran discontinuous conversion digunakan. Dengan menggunakan scan mode dan discontinuous conversion, grup channel akan dibagi-bagi menjadi beberapa subgrup sesuai dengan jumlah channel yang ditentukan. Setiap kali CPU memberi perintah ke ADC untuk mengonversi maka ADC akan mengonversi satu subgrup mulai dari subgrup pertama.
Seperti yang dijelaskan pada artikel sebelumnya Mengakses ADC STM32F103C8 dengan scan mode dan discontinuous conversion paling aman jika menggunakan Direct Memory Access (DMA). Meskipun crate stm32f1xx_hal menyediakan method untuk discontinuous conversion kenyataannya kita masih harus masih mengakses register ADC agar data yang dihasilkan reliabel. Hal ini memang agak tricky, tetapi kami akan menjelaskan secara detail dibawah kenapa hal itu diperlukan.
Pengenalan ADC Scan Mode dengan Discontinuous Conversion pada Blue Pill (STM32F103C8)
Discontinuous conversion berfungsi untuk membagi grup channel menjadi beberapa subgrup. Setiap CPU memberi perintah ke ADC untuk mengonversi, maka satu subgrup akan dikonversi mulai dari subgrup pertama. Setiap kali ADC selesai mengonversi satu channel maka akan mengeset bit EOC (End of Conversion) register SR ke 1. Untuk mengaktifkan discontinuous conversion, kita perlu menyeting register CR1 bit DISCEN ke 1 dan mengubah bit DISCNUM sesuai dengan jumlah channel yang diinginkan dalam satu subgrup. Maksimal channel dalam satu subgrup adalah 8 channel.
Bit DISCNUM terdiri dari 3 bit yang berarti hanya mampu menampung nilai dari 0 sampai 7. Nilai DISCNUM merupakan jumlah channel dalam subgrup dikurangi 1 (n-1). Jika kita ingin dalam satu subgrup berisikan 1 channel maka kita harus mengeset nilai DISCNUM ke 0 (1-1=0), begitu pula jika kita ingin subgrup berisikan 3 channel maka kita harus mengeset nilai DISCNUM ke 2 (3-1=2). Berikut merupakan ilustrasi discontinuous conversion:

Pada gambar tersebut kita membuat grup channel yang terdiri dari 7 sequence channel. Ketika discontinuous conversion diaktifkan dan DISCNUM diberi nilai 1 (dalam subgrup terdiri dari 2 channel), maka subgrup 1 sampai 3 akan berisikan 2 channel sedangkan subgrup terakhir berisikan 1 channel (sisanya). Berikut merupakan proses konversi ADC dengan discontinuous conversion:
- CPU memberikan perintah ke ADC untuk mengonversi: ADC akan mengonversi subgrup 1.
- CPU memberikan perintah kedua: ADC mengonversi subgrup 2.
- CPU memberikan perintah ketiga: ADC mengonversi subgrup 3.
- CPU memberikan perintah ke empat: ADC mengonversi subgrup terakhir (subgrup 4)
- Jika CPU memberikan perintah ke ADC lagi: ADC akan mengonversi subgrup pertama lagi.
Persiapan Hardware
Karena kita masih akan menggunakan ADC scan mode, hardware yang digunakan pada artikel ini masih sama dengan artikel sebelumnya, antara lain: Board Blue Pill (STM32F103C8), ST-Link USB Downloader Debugger, potensiometer, breadboard dan beberapa kabel jumper (female to female dan male to male). Silakan baca penjelasan fungsi komponennya di artikel ini dan ini. Anda dapat menggunakan lebih banyak potensiometer, nanti cukup menyesuaikan di bagian rangkaian dan kode program.
Input Analog Scan Mode dengan Discontinuous Conversion pada Blue Pill (STM32F103C8) Menggunakan Rust
Pada tutorial ini akan fokus pada menggunakan scan mode dengan discontinuous conversion, untuk scan mode biasa dan scan mode dengan continuous conversion dibahas pada artikel sebelumnya. Kita akan mencoba membaca nilai tegangan input dari 2 buah potensiometer dengan menggunakan 2 channel yaitu PA0 dan PA1, kemudian nilai digitalnya akan ditampilkan pada terminal PC/laptop.
Rangkaian Blue Pill (STM32F103C8) dengan Potensiometer untuk Scan Mode
Pertama silakan buat rangkaian pada breadbroad sesuai dengan skematik berikut, kami menggunakan pin PA0 (channel 0) dan pin PA1 (cahnnel 1) sebagai input analog:

Anda dapat menggunakan channel lain sesuai dengan keinginan anda, pastikan nanti menyesuaikan pada program yang akan dibuat. Jika anda menggunakan lebih banyak potensiometer, silakan sesuaikan dengan skematik tersebut: Pin 1 potensiometer dihubungkan ke GND , pin 2 ke channel ADC yang digunakan, dan pin 3 di hubungkan ke 3.3V.
Memprogram Blue Pill (STM32F103C8) sebagai Input Analog Scan Mode dengan Discontinuous Conversion
Kami akan membuat grup channel berisikan 6 sequence channel yang akan diubah menjadi dua subgrup, dimana satu subgrup terdiri dari 3 channel. Sehingga untuk mengonversi seluruh channel diperlukan dua kali perintah dari CPU.
Pertama buat project Rust baru sesuai dengan artikel ini. Tambahkan binary baru dengan nama ‘scan-mode-discontinuous’: buka file Cargo.toml lalu isi dengan kode berikut untuk mendefinisikan binary executable baru:
1[[bin]]
2name = "scan-mode-discontinuous"
3path = "src/main.rs"
4test = false
5bench = falseBuka file src/main.rs lalu ubah isinya sesuai dengan kode berikut untuk memberitahu compiler bahwa kita tidak menggunakan standard libray dan program tidak berjalan di sistem operasi:
1#![no_std]
2#![no_main]Mendefinisikan semua library/crate yang akan digunakan:
1use cortex_m_rt::entry;
2use defmt_rtt as _;
3use panic_probe as _;
4
5use stm32f1xx_hal::{
6 adc::{self, Adc, SetChannels},
7 gpio::{Analog, PA0, PA1},
8 pac::{self, ADC1},
9 prelude::*,
10 rcc::Config,
11 time::Hertz,
12};Library/crate cortex_m_rt untuk menentukan fungsi entry program akan mulai berjalan dan menangani proses startup program. defmt_rtt berfungsi untuk mengirimkan data ke PC/laptop untuk logging menggunakan protokol real time transfer . Library stm32f1xx_hal berfungsi agar kita dapat mengakses periferal mikrokontoler STM32F103C8 secara aman. panic_probe digunakan untuk menangani jika terjadi runtime error, dan akan otomatis mengirimkan log eror yang terjadi ke host PC/laptop.
Selanjutnya buat struct baru untuk menampung semua channel dalam satu grup.
1struct AdcChannels(PA0<Analog>, PA1<Analog>);Karena kami akan menggunakan pin PA0 dan pin PA1 maka kami membuat struct tuple dengan nama AdcChannels serta dengan field PA0 dan PA1 yang masing-masing bertipe Analog . Jika anda menggunakan channel lain silakan sesuaikan di sini atau jika menggunakan lebih banyak channel silakan ditambahkan di sini.
Mengimplementasikan trait SetChannel dengan tipe struct yang telah kita buat ke Adc yang bertipe ADC1.
1impl SetChannels<AdcChannels> for Adc<ADC1> {
2 fn set_samples(&mut self) {
3 self.set_channel_sample_time(0, adc::SampleTime::T_55);
4 self.set_channel_sample_time(1, adc::SampleTime::T_55);
5 }
6
7 fn set_sequence(&mut self) {
8 self.set_regular_sequence(&[0, 1, 0, 1, 0, 1]);
9 self.set_discontinuous_mode(Some(2));
10 }
11}Disini kami mengatur sample time untuk setiap channel ke 55.5 cycle. Selanjutnya membuat grup channel dengan 6 sequence channel [0, 1, 0, 1, 0, 1]. Dengan menggunakan method set_discontinuous_mode kita mengisi nilai DISCNUM dengan 2 sehingga setiap subgrup akan berisikan 3 channel yaitu [0, 1, 0] dan [1, 0, 1].
Peringatan: Ketika menggunakan method
set_discontinuous_modepastikan untuk mengisi parameterchannels_countdengan nilai jumlah channel dalam satu subgrup dikurangi 1 (n-1). Karena nilai dari paramter tersebut akan langsung dimasukkan ke bitDISCNUM. Secara awam kita mungkin mengira bahwa parameter tersebut diisi dengan jumlah channel dalam satu subgrup yang sebenarnya. Dan pastikan tidak mengisi dengan nilai lebih dari 7 karena akan menyebabkanDISCNUMbernilai tidak tentu.
Selanjutnya buat fungsi `main.
1#[entry]
2fn main() -> ! {
3 // Akses periferal,
4 // lakukan setup,
5 // dan jalankan kode program utama dalam loop
6}Di dalam fungsi main kita akan mengakses periferal Blue Pill (STM32F103C8), kemudian melakukan setup konfigurasi clock , GPIO , dan lain-lain. Dan juga membuat kode program utama di dalam blok loop.
Di dalam fungsi main tambahkan kode berikut:
1defmt::println!("ADC Scan Mode Discontinuous");
2
3let dp = pac::Peripherals::take().unwrap();
4let cp = pac::CorePeripherals::take().unwrap();Mengirimkan pesan ke terminal PC/laptop sekaligus menandai program scan mode dengan discontinuous conversion. Baris selanjutnya berfungsi untuk mengakses periferal mikrokontroler STM32F103C8 dan periferal CPU Cortex-M3 (CorePeriPherals).
1let mut flash = dp.FLASH.constrain();
2let rcc = dp.RCC.constrain();Mengakses periferal Flash dan periferal Reset & Clock Control (RCC).
1let clock_cfgr = Config::default()
2 .use_hse(Hertz::MHz(8))
3 .sysclk(Hertz::MHz(72))
4 .hclk(Hertz::MHz(72))
5 .adcclk(Hertz::MHz(9));
6
7let mut clocks = rcc.freeze(clock_cfgr, &mut flash.acr);Membuat konfigurasi clock yang menggunakan clock eksternal ( use_hse ) dengan frekuensi 8 MHz, mengatur System clock ( sysclk ) ke 72 MHz, mengatur Bus clock ( hclk ) ke 72 MHz, dan mengatur ADC clock ( adcclk ) ke 9 MHz. Kemudian menerapkan konfigurasi clock tersebut ke RCC dan menyesuaikan wait state memori flash.
1let mut delay = cp.SYST.delay(&clocks.clocks);Menggunakan System Timer untuk membuat delay.
1 let mut gpioa = dp.GPIOA.split(&mut clocks);
2
3let potentio_1 = gpioa.pa0.into_analog(&mut gpioa.crl);
4let potentio_2 = gpioa.pa1.into_analog(&mut gpioa.crl);Mengakses periperal GPIO Port A dan mengonfigurasi pin PA0 dan PA1 sebagai input analog yang akan digunakan untuk membaca tegangan input dari potensionmeter.
1let dma = dp.DMA1.split(&mut clocks);Mengakses periferal DMA1 (Direct Memory Access).
1let adc1 = adc::Adc::new(dp.ADC1, &mut clocks);Mengakses periferal ADC1.
1let adc_channels = AdcChannels(potentio_1, potentio_2);
2
3let adc_scan_dma = adc1.with_scan_dma(adc_channels, dma.1);Membuat variabel dari struct yang sudah kita buat dengan isi field: GPIO PA0 ( potentio_1 ) dan PA1 ( potentio_2 ) sebagai input analog. Selanjutnya menggunakan method with_scan_dma untuk mengonfigurasi ADC1 ke scan mode sekaligus menggunakan DMA channel 1.
1let buf = cortex_m::singleton!(: [u16; 6] = [0; 6]).unwrap();Membuat buffer untuk menyimpan hasil konversi ADC. Kami menggunakan 6 buah unsigned int 16 bit karena kami menggunakan grup channel dengan sequence 6 channel jika, jika anda menggunakan jumalh sequence yang berbeda silakan sesuaikan.
1let mut i = 0u32;Membuat variabel untuk menyimpan jumlah total ADC mengonversi seluruh channel.
1unsafe {
2 (*pac::ADC1::ptr())
3 .cr2()
4 .modify(|_, w| w.adon().clear_bit());
5}
6let mut transfer = adc_scan_dma.read(buf);Method read harusnya memerintahkan ADC untuk melakukan konversi dan menyiapkan DMA untuk mengirimkan data hasil konversi ke buffer, tapi sayangnya pada discontinuous conversion CPU gagal memerintahkan ADC untuk melakukan konversi (solusinya ada dibawah). Disini juga agak tricky karena agar data reliabel kita perlu menonaktifkan ADC (dengan mengeset register CR2 bit ADON ke 0) sebelum memanggil method read , kalau tidak data yang terisi ke buffer bisa bergeser atau acak tidak sesuai dengan sequence channel yang telah ditetapkan.
Catatan: Ketika memanggil method
readregister CR2 bitADONakan diset ke 1 lagi, sehingga kita tidak perlu secara manual mengeset bit tersebut ke 1 lagi.
Selanjutnya buat blok loop.
1loop {
2 // kode utama
3}Blok loop akan menjalankan kode program utama secara berulang.
Di dalam blok loop tambahkan kode utama kita sebagi berikut:
1for x in 0..2 {
2 // Mengonversi dan membaca nilai ADC
3}
4i += 1;Di dalam blok loop kami membuat perulangan for 2 kali karena kami memiliki 2 subgrup. Sekali perulangan for berjalan maka 1 subgrup dikonversi, begitu perulangan for berjalan 2 kali maka seluruh subgrup sudah selesai dikonversi. Jika anda memiliki jumlah subgrup yang berbeda silakan sesuaikan. Selanjutnya menambah nilai variabel i dengan 1 setiap seluruh subgrup selesai dikonversi.
Di dalam perulangan for tambahkan kode untuk memulai konversi dan membaca nilai ADC:
1unsafe {
2 (*pac::ADC1::ptr())
3 .cr2()
4 .modify(|_, w| w.swstart().set_bit());
5}
6delay.delay_ms(1u8);
7
8let group = unsafe { (*pac::DMA1::ptr()).ch(0).ndtr().read().ndt().bits() };
9
10if group == 3 {
11 defmt::println!("Convert subgroup {}: {:?}", x, transfer.peek()[..3]);
12}
13
14if group == 0 {
15 defmt::println!("Convert subgroup {}: {:?}", x, transfer.peek()[3..]);
16 }
17
18if transfer.is_done() {
19 let (recovered_buf, recovered_adc_dma) = transfer.wait();
20 defmt::println!("{} Convert full Data: {:?}", i, recovered_buf);
21
22 unsafe {
23 (*pac::ADC1::ptr())
24 .cr2()
25 .modify(|_, w| w.adon().clear_bit());
26 }
27
28 transfer = recovered_adc_dma.read(recovered_buf);
29}
30
31delay.delay_ms(200u8);Pertama mulai konversi subgrup pertama dengan mengeset bit SWSTART register CR2 ke 1 dan berikan delay 1 ms. Selanjutnya membaca nilai dari register NDTR (Number of Data Transfer) dari DMA1. Jika nilai NDTR sama dengan 3 artinya 3 data ADC sudah dipindahkan ke buffer yang menunjukkan konversi subgrup 1 selesai. Selanjutnya dengan menggunakan method peek ambil 3 data pertama dari buffer kemudian kirimkan ke terminal PC/laptop. Jika nilai NDTR sama dengan 0 artinya 6 data ADC sudah dipindahkan ke buffer yang menunjukkan konversi subgrup pertama dan subgrup kedua sudah selesai. Selanjutnya ambil 3 data terakhir dari buffer kemudian kirimkan ke terminal PC/laptop. Berikutnya jika buffer sudah penuh (semua subgrup sudah selesai dikonversi), maka dengan menggunakan method wait ambil data seluruh buffer lalu kirimkan ke terminal PC/laptop.
Selanjutnya mempersiapkan untuk melakukan konversi subgrup kedua. Set bit ADON register CR2 ke 0 agar pembacaan reliabel. Kemudian panggil method read lagi untuk mengaktifkan ADC dan mempersiapkan DMA. Berikan delay 200 ms untuk memberikan jeda di setiap perulangan. Selanjutnya perulangan for akan mulai dari awal lagi yaitu mengeset bit SWSTART register CR2 ke 1 untuk melakukan konversi subgrup kedua.
Hal ini akan terjadi secara terus berulang sehingga subgrup pertama dan subgrup kedua dikonversi secara bergantian.
Catatan: Pada DMA, setiap satu data dipindahkan ke buffer maka nilai register
NDTRakan berkurang satu dan jika buffer sudah penuh maka nilai registerNDTRakan menjadi 0.
Pada program tersebut CPU memberikan perintah ke ADC dengan mengeset bit SWSTART register CR2 ke 1 setiap akan mengonversi subgrup.
1#![no_std]
2#![no_main]
3
4use cortex_m_rt::entry;
5use defmt_rtt as _;
6use panic_probe as _;
7
8use stm32f1xx_hal::{
9 adc::{self, Adc, SetChannels},
10 gpio::{Analog, PA0, PA1},
11 pac::{self, ADC1},
12 prelude::*,
13 rcc::Config,
14 time::Hertz,
15};
16
17struct AdcChannels(PA0<Analog>, PA1<Analog>);
18
19impl SetChannels<AdcChannels> for Adc<ADC1> {
20 fn set_samples(&mut self) {
21 self.set_channel_sample_time(0, adc::SampleTime::T_55);
22 self.set_channel_sample_time(1, adc::SampleTime::T_55);
23 }
24
25 fn set_sequence(&mut self) {
26 self.set_regular_sequence(&[0, 1, 0, 1, 0, 1]);
27 self.set_discontinuous_mode(Some(2));
28 }
29}
30
31#[entry]
32fn main() -> ! {
33 defmt::println!("ADC Scan Mode Discontinuous");
34
35 let dp = pac::Peripherals::take().unwrap();
36
37 let cp = pac::CorePeripherals::take().unwrap();
38
39 let mut flash = dp.FLASH.constrain();
40
41 let rcc = dp.RCC.constrain();
42
43 let clock_cfgr = Config::default()
44 .use_hse(Hertz::MHz(8))
45 .sysclk(Hertz::MHz(72))
46 .hclk(Hertz::MHz(72))
47 .adcclk(Hertz::MHz(9));
48
49 let mut clocks = rcc.freeze(clock_cfgr, &mut flash.acr);
50
51 let mut delay = cp.SYST.delay(&clocks.clocks);
52
53 let mut gpioa = dp.GPIOA.split(&mut clocks);
54
55 let potentio_1 = gpioa.pa0.into_analog(&mut gpioa.crl);
56 let potentio_2 = gpioa.pa1.into_analog(&mut gpioa.crl);
57
58 let dma = dp.DMA1.split(&mut clocks);
59
60 let adc1 = adc::Adc::new(dp.ADC1, &mut clocks);
61
62 let adc_channels = AdcChannels(potentio_1, potentio_2);
63
64 let adc_scan_dma = adc1.with_scan_dma(adc_channels, dma.1);
65
66 let buf = cortex_m::singleton!(: [u16; 6] = [0; 6]).unwrap();
67 let mut i = 0u32;
68
69 unsafe {
70 (*pac::ADC1::ptr())
71 .cr2()
72 .modify(|_, w| w.adon().clear_bit());
73 }
74 let mut transfer = adc_scan_dma.read(buf);
75
76 loop {
77 for x in 0..2 {
78 unsafe {
79 (*pac::ADC1::ptr())
80 .cr2()
81 .modify(|_, w| w.swstart().set_bit());
82 }
83 delay.delay_ms(1u8);
84
85 let group = unsafe { (*pac::DMA1::ptr()).ch(0).ndtr().read().ndt().bits() };
86
87 if group == 3 {
88 defmt::println!("Convert subgroup {}: {:?}", x, transfer.peek()[..3]);
89 }
90
91 if group == 0 {
92 defmt::println!("Convert subgroup {}: {:?}", x, transfer.peek()[3..]);
93 }
94
95 if transfer.is_done() {
96 let (recovered_buf, recovered_adc_dma) = transfer.wait();
97 defmt::println!("{} Convert full Data: {:?}", i, recovered_buf);
98
99 unsafe {
100 (*pac::ADC1::ptr())
101 .cr2()
102 .modify(|_, w| w.adon().clear_bit());
103 }
104
105 transfer = recovered_adc_dma.read(recovered_buf);
106 }
107
108 delay.delay_ms(200u16);
109 }
110 i += 1;
111 }
112}jalankan program dengan perintah cargo run --bin scan-mode-discontinuous pada terminal. Berikut merupakan hasil dari program scan mode dengan discontinuous conversion:
Pada hasil tersebut dapat dilihat bahwa perulangan for pertama akan mengonversi subgrup 0 ([0, 1, 0]) sehingga 3 data pertama pada buffer akan berisikan nilai ADC [channel 0, channel 1, channel 0] dan perulangan for kedua akan mengonversi subgrup 1 ([1, 0, 1]) sehingga 3 data terakhir pada buffer akan berisikan nilai ADC [channel 1, channel 0, channel 1]. Seluruh subgrup selesai dikonversi dalam dua kali perintah konversi dari CPU.

Permasalahan yang Ditemui
Berikut merupakan beberapa masalah yang kami alami ketika membuat tutorial ini:
- Data yang pada buffer DMA terisi secara acak tidak sesuai dengan susunan sequence channel: Dengan melakukan debugging kami menemukan bahwa register CR1 bit
DISCNUMbernilai 3 (seharusnya 2) karena kami mengeset parameterset_discontinuous_modedengan nilai 3 (3 channel per subgrup). Setelah kami perbiki dengan mengganti nilainya ke 2 data pada buffer DMA masih terlihat bergeser. Untuk mengatasi hal tersebut kami mengeset nilai bitADONke 0 setiap sebelum methodreaddipanggil.
Source code yang digunakan pada artikel ini dapat ditemukan di repositori Github.
Jika anda mengalami kendala lain, ingin bertanya, atau menyampaikan kritik dan saran, silakan hubungi kami melalui halaman kontak.