Blue Pill (STM3F103C8): ADC Scan Mode dengan Rust

Pada artikel sebelumnya kita sudah membahas mengenai ADC pada STM32F103C8 dengan mulai dari pengenalan ADC, menggunakan mode single conversion dan hingga menggunakan mode continuous conversion . Jika pada artikel sebelum-sebelumnya kita hanya membaca satu channel saja, pada tutorial ini kita akan mencoba membaca beberapa channel dengan menggunakan scan mode.

Karena Scan mode membaca beberapa channel maka cara terbaik untuk melakukannya adalah dengan menggunakan periferal DMA (Direct Memory Access). Nanti buffer DMA akan otomatis terisi nilai dari channel-channel yang telah ditentukan. Jika membaca langsung dari DATA di register DR tidak memungkinkan karena field tersebut akan otomatis tertimpa oleh nilai channel berikutnya.

Pengenalan ADC Scan Mode pada Blue Pill (STM32F103C8)

Pada scan mode, ADC STM32F103C8 akan mengonversi daftar beberapa channel yang telah ditentukan satu per satu kemudian berhenti. CPU cukup memberi perintah sekali saja ke ADC untuk melakukan konversi, kemudian ADC akan secara otomatis mengonversi mulai dari channel pertama sampai terkahir di dalam grup channel yang telah ditentukan (CPU tidak perlu lagi memerintahkan ADC untuk mengonversi channel-channel berikutnya). ADC STM32F103C8 dalam scan mode dapat juga di kombinasikan dengan mode continuous conversion dan mode discontinuous conversion:

  • Scan mode dengan continuous conversion: Jika dikombinasikan dengan mode scan conversion maka ADC setelah selesai melakukan konversi terakhir pada grup channel yang ditentukan, ADC akan secara otomatis kembali mengonversi channel pertama hingga terakhir secara terus menerus.
  • Scan mode dengan discontinuous conversion: Jika dikombinasikan dengan mode discontinuous conversion maka grup channel akan dibagi menjadi beberapa subgrup sesuai dengan yang kita tentukan. Ketika CPU memerintahkan ADC melakukan konversi, maka ADC akan mengonversi dari channel pertama subgrup pertama sampai channel terakhir subgrup pertama, untuk mengonversi subgrup selanjutnya ADC perlu perintah lagi dari CPU. Namu jika dikombinasikan lagi dengan mode continuous conversion maka setelah selesai mengonversi subgrup pertama maka akan otomatis mengonversi subgrup selanjutnya dan jika subgrup terakhir sudah selesai dikonversi maka akan kembali mengonversi subgrup pertama secara terus menerus. Setiap selesai melakukan konversi setiap channel di setiap subgrup maka ADC akan mengeset bit EOC (End of Conversion) register SR ke 1.

Persiapan Hardware

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). Namun karena pada tutorial ini kita akan membaca beberapa channel sekaligus maka kami akan menggunakan 2 buah potensiometer sebagai input. Anda dapat menggunakan lebih banyak potensiometer, nanti cukup menyesuaikan di bagian rangkaian dan kode program.

Input Analog Scan Mode pada Blue Pill (STM32F103C8) dengan Rust

Pada tutorial ini kita akan mencoba membaca nilai tegangan input dari 2 buah potensiometer dengan menggunakan 2 channel yaitu PA0 dan PA1. nanti nilai digitalnya akan ditampilkan pada terminal PC/laptop. Pada tutorial ini akan menggunakan scan mode biasa dan scan mode dengan continuous conversion, Untuk scan mode dengan discontinuous conversion akan dibahas pada artikel selanjutnya.

Rangkaian Blue Pill (STM32F103C8) dengan Potensiometer untuk Scan Mode

Kita masih akan menggunakan potensiometer sebagai perangkat pembagi tegangan yang akan bertindak seperti sensor yang dihubungkan ke pin GPIO PA0 dan PA1. Berikut merupakan rangkaian skematik yang digunakan untuk mencoba scan mode:

Rangkaian potensiometer dan STM32 Blue Pill untuk input analog dengan scan mode
Rangkaian potensiometer dan STM32 Blue Pill untuk input analog dengan scan mode

Silakan buat rangkaian seperti skematik di breadboard tersebut, sebelum kita lanjut ke tahap membuat program untuk ADC scan mode. 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 dengan Scan Mode

Pertama mari kita buat project Rust baru sesuai dengan artikel ini . Tambahkan binary baru dengan nama ‘scan-mode’: buka file Cargo.toml lalu isi dengan kode berikut:

1[[bin]]
2name = "scan-mode"
3path = "src/main.rs"
4test = false
5bench = false

Selanjutnya buka 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]

Definisikan semua library yang akan kita gunakan dengan menambahkan kode berikut:

 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 lebih banyak channel silakan ditambahkan di sini, atau jika anda menggunakan channel yang lain silakan sesuaikan.

Implementasikan 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
4    fn set_sequence(&mut self) {
5        self.set_regular_sequence(&[0, 1]);
6    }
7}

Pada method set_samples kita bisa mengatur Sample Time tiap-tiap channel, namun disini kamu menggunakan sample time default. Pada method set_sequence kita mengatur urutan channel akan dikonversi/dibaca. Disini kami menggunakan urutan chanel 0 setelah itu channel 1, anda dapat menyesuaikan sesuai dengan keinginan anda.

Pro Tips: Urutan channel ADC bisa juga dibuat secara acak atau berulang. Sebagai contoh: [0, 1, 0, 0, 1, 0], nantinya ADC akan mengonversi dari channel 0 ke 1, kemudian ke 0 lagi dan seterusnya sampai channel terakhir (channel 0). Kita juga harus menyesuaikan buffer DMA dengan jumlah urutan channel tersebut. Misalnya dalam contoh urutan channel [0, 1, 0, 0, 1, 0], maka sesuaikan buffer DMA untuk menampung 6 channel bukan 2 channel.

Buat fungsi main sebagai program entry (titik mulai program dijalankan):

1#[entry]
2fn main() -> ! {
3    // Akses periferal disini,
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 membuat kode program utama di dalam blok loop.

Di dalam fungsi main tambahkan kode berikut:

1defmt::println!("ADC Scan Mode");
2
3let dp = pac::Peripherals::take().unwrap();
4let cp = pac::CorePeripherals::take().unwrap();

Baris pertama berfungsi untuk mengirimkan pesan ke terminal PC/laptop sekaligus menandai program scan mode. Baris selanjutnya berfungsi untuk mengakses periferal mikrokontroler STM32F103C8 dan periferal CPU Cortex-M3 (CorePeriPherals) dan menyimpannya masing-masing di variabel dp dan cp .

1let mut flash = dp.FLASH.constrain();
2let rcc = dp.RCC.constrain();

Mengakses periferal Flash dan periferal Reset & Clock Control (RCC). Periferal Flash diperlukan untuk menyesuaikan wait state ketika konfigurasi clock. Periferal RCC digunakan untuk mengonfigurasi clock mikrokontoler STM32F103C8.

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 sekaligus menyesuaikan wait state memori flash.

1let mut delay = cp.SYST.delay(&clocks.clocks);

Membuat instance delay dengan menggunakan System Timer untuk memberikan waktu jeda pada perulangan kode utama.

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) untuk kita gunakan nanti dengan ADC.

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 ) sebagain input analog. Selanjutnya menggunakan method with_scan_dma untuk mengonfigurasi ADC1 ke scan mode sekaligus menggunakan DMA channel 1.

Catatan: Hanya DMA channel 1 yang bisa digunakan dengan ADC dan hanya periferal ADC1 yang bisa menggunakan DMA. Pembahasan mengenai DMA akan dibahas pada artikel berikutnya.

1let buf = cortex_m::singleton!(: [u16; 2] = [0; 2]).unwrap();
2
3let mut transfer = adc_scan_dma.read(buf);

Membuat buffer untuk menyimpan hasil konversi ADC. Kami menggunakan 2 buah unsigned int 16 bit karena kami hanya menggunakan 2 channel (index 0 untuk channel pertama dan index 1 untuk channel kedua), jika anda menggunakan channel lebih banyak silakan sesuaikan dengan jumlah channel yang anda gunakan. Baris selanjutnya berfungsi memerintahkan ADC untuk melakukan konversi dari channel pertama hingga channel kedua, nanti secara otomatis menggunakan DMA mengirimkan data hasil konversi ke buffer yang kita buat.

Selanjutnya buat blok loop, dimana kode utama kita dijalankan secara berulang.

1loop {
2    // kode utama
3}

Blok loop yang akan menjalankan kode program utama secara berulang.

Di dalam blok loop tambahka kode utama kita sebagi berikut:

 1let (recovered_buf, recovered_adc_dma) = transfer.wait();
 2
 3defmt::println!(
 4    "Potentio 1: {}, Potentio 2: {}",
 5    recovered_buf[0],
 6    recovered_buf[1]
 7);
 8
 9transfer = recovered_adc_dma.read(recovered_buf);
10delay.delay_ms(200u16);

Baris pertama berfungsi untuk membaca nilai ADC dari buffer DMA jika sudah terisi. Selanjutnya nilai tersebut dikirimkan ke terminal PC/laptop. Kemudian CPU memerintahkan lagi ADC untuk melakukan konversi dengan method read . Baris terakhir berfungsi untuk memberikan waktu jeda 200 ms di setiap perluangan.

Setiap kali CPU ingin membaca nilai ADC, CPU perlu memberikan perintah ke ADC untuk melakukan konversi. Untuk menghindari hal ini scan mode dapat dikombinasikan dengan continuous conversion seperti yang akan dibahas di bawah ini.

Tampilkan kode full: ADC Scan Mode
 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
22    fn set_sequence(&mut self) {
23        self.set_regular_sequence(&[0, 1]);
24    }
25}
26
27#[entry]
28fn main() -> ! {
29    defmt::println!("ADC Scan Mode");
30
31    let dp = pac::Peripherals::take().unwrap();
32
33    let cp = pac::CorePeripherals::take().unwrap();
34
35    let mut flash = dp.FLASH.constrain();
36
37    let rcc = dp.RCC.constrain();
38
39    let clock_cfgr = Config::default()
40        .use_hse(Hertz::MHz(8))
41        .sysclk(Hertz::MHz(72))
42        .hclk(Hertz::MHz(72))
43        .adcclk(Hertz::MHz(9));
44
45    let mut clocks = rcc.freeze(clock_cfgr, &mut flash.acr);
46
47    let mut delay = cp.SYST.delay(&clocks.clocks);
48
49    let mut gpioa = dp.GPIOA.split(&mut clocks);
50
51    let potentio_1 = gpioa.pa0.into_analog(&mut gpioa.crl);
52    let potentio_2 = gpioa.pa1.into_analog(&mut gpioa.crl);
53
54    let dma = dp.DMA1.split(&mut clocks);
55
56    let adc1 = adc::Adc::new(dp.ADC1, &mut clocks);
57
58    let adc_channels = AdcChannels(potentio_1, potentio_2);
59
60    let adc_scan_dma = adc1.with_scan_dma(adc_channels, dma.1);
61
62    let buf = cortex_m::singleton!(: [u16; 2] = [0; 2]).unwrap();
63
64    let mut transfer = adc_scan_dma.read(buf);
65
66    loop {
67        let (recovered_buf, recovered_adc_dma) = transfer.wait();
68
69        defmt::println!(
70            "Potentio 1: {}, Potentio 2: {}",
71            recovered_buf[0],
72            recovered_buf[1]
73        );
74
75        transfer = recovered_adc_dma.read(recovered_buf);
76        delay.delay_ms(200u16);
77    }
78}

Jalan program dengan perintah ‘cargo run --bin scan-mode’ di terminal VSCode. Berikut merupakan hasil dari program tersebut:

Dari hasil percobaan terlihat bahwa begitu kita memutar knob pada potensiometer 1 maka nilai ADC pada channel 0 (PA0) juga akan berubah, begitu juga pada potensiometer 2 ketika diputar knobnya maka nilai ADC channel 1 (PA1) juga akan berubah.

Memprogram Blue Pill (STM32F103C8) sebagai Input Analog dengan Scan Continuous Mode

Pada bagian ini kita akan mengombinasikan scan mode dengan continuous conversion, sehingga CPU tidak perlu memerintahkan ADC untuk melakukan konversi secara terus menerus dalam loop.

Pertama mari definisikan binary executable baru dengan nama ‘scan-mode-continuous’ pada project Rust yang sudah dibuat:

1[[bin]]
2name = "scan-mode-continuous"
3path = "src/scan_mode_continuous.rs"
4test = false
5bench = false

Buat file src/scan_mode_continuous.rs. Kode yang akan kita gunakan pada section ini sama dengan pada section sebelumnya, yang membedakannya pada saat implementasi trait SetCahnnels, membuat buffer DMA, pada proses delay, dan proses membaca nilai ADC.

Berikut kode untuk implementasi trait SetCahnnels:

 1struct AdcChannels(PA0<Analog>, PA1<Analog>);
 2
 3impl SetChannels<AdcChannels> for Adc<ADC1> {
 4    fn set_samples(&mut self) {
 5        self.set_channel_sample_time(0, adc::SampleTime::T_239);
 6        self.set_channel_sample_time(1, adc::SampleTime::T_239);
 7    }
 8
 9    fn set_sequence(&mut self) {
10        self.set_regular_sequence(&[0, 1]);
11        self.set_continuous_mode(true);
12    }
13}

Pada method set_samples kita mengatur sample time untuk setiap channel ke 239.5 cycles. Sedangkan pada method set_sequence kita menambahkan kode untuk mengaktifkan mode continuous conversion.

Membuat buffer DMA untuk menyimpan nilai ADC.

1let buffer = cortex_m::singleton!(: [[u16; 200]; 2] = [[0; 200]; 2]).unwrap();

Karena kita akan menggunakan method circular_read maka kita butuh array yang berisikan 2 item untuk bagian pertama dan bagian kedua. Nantinya ketika CPU membaca bagian pertama maka DMA akan mengisi bagian kedua. masing-masing bagian kami buat berupa array unsigned int 16 bit dengan panjang 200, yang dimana 100 untuk masing-masing channel. Jika anda menggunakan lebih banyak channel silakan sesuaikan dengan jumlah channel yang anda gunakan. Pada masing-masing bagian (baik pertama maupun kedua) array akan terisi nilai ADC secara selang seling sesuai dengan urutan channel yang telah ditentukan pada method set_sequence , seperti berikut: [channel0, channel1, channeln, ..., channel0, channel1, channeln].

Mempersiapkan System Timer sebagain counter untuk mengatur waktu mengirimkan hasil ADC ke terminal PC/laptop.

1let mut counter = cp.SYST.counter_us(&clocks.clocks);
2let _ = counter.start(230_000.micros()).unwrap();
3let mut last_update_time = counter.now();
4let interval = 200_000u32.micros::<1, 1_000_000>();

Membuat counter yang akan kita gunakan sebagai waktu jeda (delay), karena ketika menggunakan circular read kita tidak bisa menggunakan delay blocking. Kami menggunakan System Timer untuk membuat counter yang bertambah tiap mikrodetik dengan nilai maksimal 230000 mikrodetik (230 ms). Kita juga butuh variable last_update_time untuk menyimpan nilai counter terakhir dan variabel interval untuk menentukan lama waktu jeda disini kami menggunakan 200000 mikrodetik (200 ms).

Catatan: Kita tidak bisa menggunakan delay blocking pada saat menggunakan circula_read karena akan mempengaruhi waktu pembacaan ke buffer DMA, sehingga rawan terjadi eror overrun .

Proses membaca nilai ADC:

 1let mut circ_buffer = adc_scan_dma.circ_read(buffer);
 2loop {
 3    if let Ok(_) = circ_buffer.readable_half() {
 4        // nilai rata-rata untuk masing-masing channel
 5        let avgs = circ_buffer.peek(|data, _h| {
 6            let mut sum_ch0 = 0u32;
 7            let mut sum_ch1 = 0u32;
 8
 9            // Data dalam buffer: [ch0, ch1, ch2, ch0, ch1, ch2, ...]
10            for chunk in data.chunks_exact(2) {
11                sum_ch0 += chunk[0] as u32;
12                sum_ch1 += chunk[1] as u32;
13            }
14
15            let len = (data.len() / 2) as u32;
16            [(sum_ch0 / len) as u16, (sum_ch1 / len) as u16]
17        });
18
19        // Program mengirim nilai ADC ke terminal PC/laptop
20        // ...
21
22    }
23}

Baris pertama memerintahkan ADC untuk melakukan konversi dan menggunakan DMA untuk menyimpan nilainya ke buffer DMA. Selanjutnya mengecek kondisi buffer, jika sudah bisa dibaca sebagian (baik bagian pertama maupun kedua) maka ambil nilai rata-rata dari masing-masing channel. Seperti yang dijelaskan sebelumnya urutan nilai ADC dalam buffer DMA akan selang-seling sesuai urutan channel yang telah ditentukan. Oleh karena itu kami menggunakan method chunks_exact untuk mengambil nilai secara selang-seling, silakan sesuaikan parameternya sesuai dengan jumlah channel yang anda gunakan. Sesuaikan juga nilai pembagi saat mencari nilai len dengan jumlah channel yang anda gunakan, disini kami menggunakan (data.len() / 2) karena kami hanya menggunakan 2 channel.

Mengirim nilai ADC ke terminal PC/laptop:

1let now = counter.now();
2
3if last_update_time.ticks().wrapping_sub(now.ticks()) >= interval.ticks() {
4    if let Ok(a) = avgs {
5        defmt::println!("Potentio 1: {} | Potentio 2: {}", a[0], a[1],);
6    }
7    last_update_time = now;
8}

Pertama kita simpan nilai counter saat ini ke variabel now . Kemudian kurangkan variabel last_update_time dengan now , jika lebih besar dari interval maka nilai ADC akan dikirim ke terminal PC/laptop. Dengan begitu program akan mengirim data setiap 200000 mikrodetik (200 ms) ke terminal PC/laptop. Terkahir perbarui nilai variabel last_update_time dengan nilai now .

Dengan menggunakan mode continuous conversion, CPU hanya perlu memerintahkan sekali ADC untuk melakukan konversi. selajutnya ADC akan otomatis melakukan konversi secara berulang-ulang.

Tampilkan kode full: ADC Scan Mode dengan Continuous Conversion
  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_239);
 22        self.set_channel_sample_time(1, adc::SampleTime::T_239);
 23    }
 24
 25    fn set_sequence(&mut self) {
 26        self.set_regular_sequence(&[0, 1]);
 27        self.set_continuous_mode(true);
 28    }
 29}
 30
 31#[entry]
 32fn main() -> ! {
 33    defmt::println!("ADC Scan Mode Continuous");
 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 gpioa = dp.GPIOA.split(&mut clocks);
 52
 53    let potentio_1 = gpioa.pa0.into_analog(&mut gpioa.crl);
 54    let potentio_2 = gpioa.pa1.into_analog(&mut gpioa.crl);
 55
 56    let dma = dp.DMA1.split(&mut clocks);
 57
 58    let adc1 = adc::Adc::new(dp.ADC1, &mut clocks);
 59
 60    let adc_channels = AdcChannels(potentio_1, potentio_2);
 61
 62    let adc_scan_dma = adc1.with_scan_dma(adc_channels, dma.1);
 63
 64    let buffer = cortex_m::singleton!(: [[u16; 200]; 2] = [[0; 200]; 2]).unwrap();
 65
 66    let mut circ_buffer = adc_scan_dma.circ_read(buffer);
 67
 68    let mut counter = cp.SYST.counter_us(&clocks.clocks);
 69    let _ = counter.start(230_000.micros()).unwrap();
 70    let mut last_update_time = counter.now();
 71    let interval = 200_000u32.micros::<1, 1_000_000>();
 72
 73    loop {
 74        if let Ok(_) = circ_buffer.readable_half() {
 75            // nilai rata-rata untuk masing-masing channel
 76            let avgs = circ_buffer.peek(|data, _h| {
 77                let mut sum_ch0 = 0u32;
 78                let mut sum_ch1 = 0u32;
 79
 80                // Data tersimpan dalam buffer: [ch0, ch1, ch2, ch0, ch1, ch2, ...]
 81                for chunk in data.chunks_exact(2) {
 82                    sum_ch0 += chunk[0] as u32;
 83                    sum_ch1 += chunk[1] as u32;
 84                }
 85
 86                let len = (data.len() / 2) as u32;
 87                [(sum_ch0 / len) as u16, (sum_ch1 / len) as u16]
 88            });
 89
 90            let now = counter.now();
 91
 92            if last_update_time.ticks().wrapping_sub(now.ticks()) >= interval.ticks() {
 93                if let Ok(a) = avgs {
 94                    defmt::println!("Potentio 1: {} | Potentio 2: {}", a[0], a[1],);
 95                }
 96                last_update_time = now;
 97            }
 98        }
 99    }
100}

Jalan program dengan perintah ‘cargo run --bin scan-mode-continuous’ di terminal VSCode. Berikut merupakan hasil percobaan kita:

Source Code dan Kesimpulan

Cara terbaik untuk membaca nilai ADC multichannel pada Blue Pill (STM32F103C8) adalah dengan menggunakan scan mode yang dikombinasikan dengan periferal DMA (Direct Memory Access). Selain itu scan mode juga bisa dikombinasikan dengan mode continuous conversion, sehingga CPU tidak perlu sibuk memerintahkan ADC untuk melakukan konversi secara berulang.

Source code yang digunakan pada artikel ini dapat ditemukan di repositori Github .

Jika anda mengalami kendala, ingin bertanya, atau menyampaikan kritik dan saran, silakan hubungi kami melalui halaman kontak .