Blue Pill (STM3F103C8): ADC Continuous Conversion dengan Rust

Sebelum membaca artikel ini, mohon untuk membaca Cara Membaca Input Analog (ADC) pada STM32 Blue Pill dengan Rust terlebih dahulu. Pada artikel tersebut dibahas mengenai input analog dan Analog to Digital Converter (ADC) pada Blue Pill (STM32F103C8), dan contoh program untuk mode Single Conversion menggunakan bahasa Rust. Pada tutorial ini kita akan lanjut mencoba menggunakan ADC pada Blue Pill (STM32F103C8) dengan mode continuous conversion menggunakan bahasa Rust.

Perbedaan antara ADC STM32F103C8 mode single conversion dan mode continuous conversion adalah pada saat mengonversi. Pada mode single conversion ADC akan melakukan konversi ketika menerima perintah dari CPU dan berhenti ketika sudah selesai. Sedangkan pada continuous conversion, setelah ADC menerima perintah konversi dari CPU pertama kali, ADC tidak akan berhenti ketika sudah selesai mengonversi, melainkan akan melakukan konversi lagi secara terus menerus tanpa menunggu perintah dari CPU lagi.

Persiapan Hardware

Hardware yang digunakan pada tutorial kali ini sama dengan hardware pada artikel sebelumnya. Hardware tersebut antara lain: Blue Pill (STM32F103C8), ST-Link USB Downloader Debugger, potensiometer, breadboard dan beberapa kabel jumper (female to female dan male to male). Penjelasan mengenai fungsinya masing-masing dapat dilihat pada artikel sebelumnya dan pada artikel setup STM32F103C8 dengan Rust di Linux.

Input Analog Mode Continuous Conversion pada Blue Pill (STM32F103C8) dengan Rust

Sama sepeti single conversion, kita juga bisa menggunakan continuous conversion dengan metode poll, Direct Memory Access (DMA), dan Interrupt:

  • Continuous Conversion Poll: ADC hanya akan melakukan konversi ketika diberi perintah oleh CPU, setelah selesai melakukan konversi ADC akan mulai lagi melakukan konversi secara terus menerus tanpa menunggu perintah dari CPU lagi. CPU akan membaca data hasil konversi di program utama tanpa perlu memerintahkan ADC untuk melakukan konversi lagi.
  • Continuous Conversion DMA: ADC akan melakukan konversi ketika diberi perintah oleh CPU, kemudian hasil konversi akan dikirim ke RAM (buffer DMA). Selama proses konversi, CPU bisa melakukan pekerjaan lain, begitu buffer DMA terisi maka CPU membaca hasil konversi dari buffer DMA. ADC akan secara terus menerus melakukan konversi dan dengan DMA akan otomatis mengisi buffer DMA tanpa menunggu perintah dari CPU lagi.
  • Continuous Conversion Interrupt: ADC akan melakukan konversi ketika diberi perintah oleh CPU, setelah konversi selesai maka ADC akan memberitahu CPU dan CPU akan mengehentikan sementara program utamanya untuk mengeksekusi program ISR (Interupt Service Routine). ADC akan melakukan konversi secara terus menerus tanpa perlu diberi perintah lagi oleh CPU. Jika kita melakukan konversi terlalu banyak tiap detiknya dengan metode ini maka CPU akan terus melakukan interupsi yang malah membebani CPU.

Sama seperti artikel sebelumnya kita akan mencoba membaca nilai tegangan input dari potensiometer menggunakan pin GPIO PA0 yang kemudian nilai digitalnya akan ditampilkan pada terminal PC/laptop.

Rangkaian Blue Pill (STM32F103C8) dengan Potensiometer

Rangkaian yang digunakan pada tutorial ini sama dengan artikel sebelumnya, kita akan memanfaatkan potensiometer sebagai perangkat pembagi tegangan yang akan bertindak seperti sensor. Berikut merupakan gambar skematik rangkaian yang digunakan:

Rangkaian potensiometer dan STM32 Blue Pill untuk Input Analog
Rangkaian potensiometer dan STM32 Blue Pill untuk Input Analog

Penjelasan lebih lanjut mengenai rangkaian tersebut dapat dilihat di artikel sebelumnya. Silakan buat rangkaian pada breadboard sesuai dengan gambar skematik tersebut, sebelum kita lanjut untuk membuat kode program Rust.

Memprogram Blue Pill (STM32F103C8) Sebagai Input Analog dengan Continuous Conversion Poll

Pertama mari buat project Rust baru seperti pada Artikel ini. Buka file Cargo.toml lalu tambahkan kode berikut untuk menambahkan binary executable baru dengan nama ‘continuous-conversion-poll’:

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

Selanjutnya kita akan membuat kode Rust untuk continuous conversion poll. Karena crate stm32f1xx_hal tidak menyediakan method untuk mengakses ADC dengan mode continuous conversion poll, maka kita akan mencobanya dengan mengakses register ADC secara langsung.

Ayo buka file src/main.rs lalu ubah isinya sesuai dengan kode berikut untuk memberitahu compiler bahwa kita tidak menggunakan standard libray dan tidak menjalankan program diatas sistem operasi:

1#![no_std]
2#![no_main]

Selanjutnya tambahkan kode berikut:

 1use cortex_m_rt::entry;
 2
 3use defmt_rtt as _;
 4use panic_probe as _;
 5use stm32f1xx_hal::{
 6    adc::{self, ChannelTimeSequence},
 7    flash::FlashExt,
 8    gpio::{GpioExt, PinExt},
 9    hal::delay::DelayNs,
10    pac,
11    rcc::{Config, RccExt},
12    time::Hertz,
13    timer::SysTimerExt,
14};

Kode tersebut berfungsi untuk mendefinisikan libray-libray yang kita gunakan. Library/crate cortex-m-rt untuk menentukan fungsi entry program akan mulai berjalan dan menangani proses startup program. defmt_rtt berfungsi untuk membantu mengirim data ke PC/laptop untuk logging dengan efisien menggunakan protokol real time transfer . panic_probe digunakan untuk menangani jika terjadi runtime error, dan akan otomati memberikan log error apa yang terjadi ke host PC/laptop. Library stm32f1xx_hal berfungsi agar kita dapat mengakses periferal mikrokontoler STM32F103C8 secara aman.

Selanjutnya kita membuat fungsi main.

1#[entry]
2fn main() -> ! {
3    // Kode logik kita
4    // ...
5}

Fungsi main tersebut kita beri macro entry yang berfungsi untuk menentukan bahwa dari fungsi ini program akan mulai dijalankan. Di dalam fungsi main ini kita kan mengakses periferal STM32F103C8 dan membuat kode logik.

Di dalam fungsi main kita isi dengan kode berikut:

1defmt::println!("Input Analog Mode Continuous Poll");

Berfungsi untuk mengirim pesan ke PC/Laptop, sekaligus menandai program mode Continuous Conversion Poll.

1let dp = pac::Peripherals::take().unwrap();
2let cp = pac::CorePeripherals::take().unwrap()

Berfungsi untuk mengakses periferal STM32F103C8 dan periferal CPU Cortex-M3 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 & control clock (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. Delay akan kita gunakan untuk memberikan waktu tunggu saat program berjalan.

1let mut gpioa = dp.GPIOA.split(&mut clocks);
2let potentio = gpioa.pa0.into_analog(&mut gpioa.crl);

Mengakses periperal GPIO Port A dan mengonfigurasi pin PA0 sebagai input analog yang akan digunakan untuk membaca tegangan input dari potensionmeter.

1let mut adc1 = adc::Adc::new(dp.ADC1, &mut clocks);
2adc1.set_continuous_mode(true);

Mengakses periferal ADC1 dan mengaturnya ke mode continuous conversion.

1let adc_reg = unsafe { &*pac::ADC1::ptr() };

Mengakses pointer register ADC1 secara langsung agar kita bisa mengubah bit registernya untuk menyesuaikan ke mode continuous conversion dengan metode poll.

1adc_reg.cr2().modify(|_, w| w.adon().clear_bit());

Mengatur register CR2 bit ADON ke 0 untuk mematikan ADC agar aman saat mengonfigurasi bit register yang lain.

1adc_reg.cr1().modify(|_, w| w.scan().clear_bit());

Menonaktifkan Mode Scan Conversion, karena kita hanya akan menggunakan 1 channel. Register CR1 bit SCAN akan bernilai 0.

1adc_reg.cr1().modify(|_, w| w.discen().clear_bit());

Menonaktifkan mode Discontinuous Conversion agar mode continuous conversion dapat berjalan. Mode Discontinuous Conversion otomatis diaktifkan ketika kita memanggil fungsi Adc::new . Register CR1 bit DISCEN akan bernilai 0.

1adc_reg.sqr1().modify(|_, w| unsafe { w.l().bits(0b0000) });

Menentukan jumlah channel yang digunakan, karena kita menggunakan 1 channel maka diisi dengan 0 (jumlah channel-1).

1adc_reg.cr2().modify(|_, w| w.adon().set_bit());

Menyalakan kembali ADC1. Register CR2 bit ADON akan bernilai 1.

1adc_reg
2    .sqr3()
3    .modify(|_, w| unsafe { w.sq1().bits(potentio.pin_id()) });

Mengatur channel ADC yang akan dibaca. Perintah potentio.pin_id() akan mengembalikan nilai pin ID dari pin PA0 yaitu 0.

1// adc_reg.cr2().modify(|_, w| w.adon().set_bit());
2adc_reg.cr2().modify(|_, w| w.swstart().set_bit());

Memulai konversi dengan mengatur register CR2 bit SWSTART ke 1. Kita juga bisa memulai konversi dengan mengatur register CR2 bit ADON ke 1 sekali lagi. Silakan pilih salah satu sesuai dengan keinginan anda.

1loop {
2    // Kode utama
3    // yang dijalankan berulang-ulang
4    // ...
5}

Merupakan blok perulangan. Di dalam blok tersebut kita akan menaruh program utama kita, sehingga akan terus dijalankan secara berulang-ulang.

1if adc_reg.sr().read().eoc().bit_is_set() {
2    // Baca nilai ADC
3}

Kode ini kita taruh dalam blok loop agar dijalankan secara berulang-ulang. Kode tersebut berfungsi untuk memmbaca nilai register SR bit EOC , jika nilainya 1 (selesai melakukan konversi) maka jalankan perintah kode di dalam blok if.

1let val = adc_reg.dr().read().bits();
2let voltage = (val as f32 / 4095.0) * 3.3;
3defmt::println!("Raw Avg: {} | Volt: {}V", val, voltage);
4delay.delay_ms(500);

Di dalam blok if kita isi dengan kode ini, yang berfungsi untuk membaca nilai ADC dari DATA register DR yang disimpan dalam variabel val . Kemudian menghitung besar tegangan input yang disimpan dalam variabel voltage . Selanjutnya nilai variabel val dan voltage dikirimkan ke terminal PC/laptop, dan kita beri jeda selama 500 ms.

Tampilkan kode full: Mode Continuous Conversion Poll
 1#![no_std]
 2#![no_main]
 3
 4use cortex_m_rt::entry;
 5
 6use defmt_rtt as _;
 7use panic_probe as _;
 8use stm32f1xx_hal::{
 9    adc::{self, ChannelTimeSequence},
10    flash::FlashExt,
11    gpio::{GpioExt, PinExt},
12    hal::delay::DelayNs,
13    pac,
14    rcc::{Config, RccExt},
15    time::Hertz,
16    timer::SysTimerExt,
17};
18
19#[entry]
20fn main() -> ! {
21    defmt::println!("Input Analog Mode Continuous Poll");
22
23    let dp = pac::Peripherals::take().unwrap();
24
25    let cp = pac::CorePeripherals::take().unwrap();
26
27    let mut flash = dp.FLASH.constrain();
28    let rcc = dp.RCC.constrain();
29
30    let clock_cfgr = Config::default()
31        .use_hse(Hertz::MHz(8))
32        .sysclk(Hertz::MHz(72))
33        .hclk(Hertz::MHz(72))
34        .adcclk(Hertz::MHz(9));
35
36    let mut clocks = rcc.freeze(clock_cfgr, &mut flash.acr);
37
38    let mut delay = cp.SYST.delay(&clocks.clocks);
39
40    let mut gpioa = dp.GPIOA.split(&mut clocks);
41    let potentio = gpioa.pa0.into_analog(&mut gpioa.crl);
42
43    let mut adc1 = adc::Adc::new(dp.ADC1, &mut clocks);
44
45    adc1.set_continuous_mode(true);
46
47    let adc_reg = unsafe { &*pac::ADC1::ptr() };
48
49    adc_reg.cr2().modify(|_, w| w.adon().clear_bit());
50
51    adc_reg.cr1().modify(|_, w| w.scan().clear_bit());
52
53    adc_reg.cr1().modify(|_, w| w.discen().clear_bit());
54
55    adc_reg.sqr1().modify(|_, w| unsafe { w.l().bits(0b0000) });
56
57    adc_reg.cr2().modify(|_, w| w.adon().set_bit());
58
59    adc_reg
60        .sqr3()
61        .modify(|_, w| unsafe { w.sq1().bits(potentio.pin_id()) });
62
63    // adc_reg.cr2().modify(|_, w| w.adon().set_bit());
64    adc_reg.cr2().modify(|_, w| w.swstart().set_bit());
65
66    loop {
67        if adc_reg.sr().read().eoc().bit_is_set() {
68
69            let val = adc_reg.dr().read().bits();
70
71            let voltage = (val as f32 / 4095.0) * 3.3;
72
73            defmt::println!("Raw ADC: {} | Volt: {}V", val, voltage);
74            delay.delay_ms(500);
75        }
76    }
77}

Jalankan program dengan perintah ‘cargo run --bin continuous-conversion-poll’ pada terminal VSCode. Berikut merupakan hasilnya:

Memprogram Blue Pill (STM32F103C8) Sebagai Input Analog dengan Continuous Conversion DMA

Pertama mari kita definisikan binary executable baru di project Rust yang sudah kita buat sebelumnya. Kita akan beri nama continuous-conversion-dma, buka file Cargo.toml lalu tambahkan kode berikut:

1[[bin]]
2name = "continuous-conversion-dma"
3path = "src/continuous_conversion_dma.rs"
4test = false
5bench = false

Selanjutnya mari kita buat programnya. Kali ini kita akan menggunakan method yang sudah disediakan oleh crate stm32f1xx_hal dan method circular_read . Pada circular read CPU bisa membaca setengah buffer pertama sementara DMA mengisi setengah buffer kedua atau sebaliknya sehingg bersifat nonblocking.

Buka file src/continuous_conversion_dma.rs lalu tambahkan kode berikut untuk mendefinisikan libray dan modul yang digunakan:

 1#![no_std]
 2#![no_main]
 3
 4use cortex_m_rt::entry;
 5
 6use defmt_rtt as _;
 7use panic_probe as _;
 8use stm32f1xx_hal::{
 9    adc::{self},
10    dma::{CircReadDma, DmaExt},
11    flash::FlashExt,
12    gpio::GpioExt,
13    pac,
14    prelude::*,
15    rcc::{Config, RccExt},
16    time::Hertz,
17    timer::SysTimerExt,
18};

Pastikan anda menggunakan menggunakan modul stm32f1xx_hal::dma::{CircReadDma, DmaExt} karena kita akan menggunakan periferal Direct Memory Access (DMA).

Selanjutnya di dalam fungsi main tambahkan kode berikut untuk mengakses periferal STM32F103C8 dan periferal CPU Cortex-M3:

1defmt::println!("Input Analog Mode Continuous DMA");
2
3let dp = pac::Peripherals::take().unwrap();
4let cp = pac::CorePeripherals::take().unwrap();

Untuk mengirim pesan ke terminal PC/laptop, kemudian mengakses periferal STM32F103C8 dan periferal CPU Cortex-M3.

 1let mut flash = dp.FLASH.constrain();
 2let rcc = dp.RCC.constrain();
 3
 4let clock_cfgr = Config::default()
 5        .use_hse(Hertz::MHz(8))
 6        .sysclk(Hertz::MHz(72))
 7        .hclk(Hertz::MHz(72))
 8        .adcclk(Hertz::MHz(9));
 9
10let mut clocks = rcc.freeze(clock_cfgr, &mut flash.acr);

Mengakses periferal flash dan periferal reset & control clock (RCC). Kemudian mengonfigurasi clock ke clock eksternal sebesar 8 MHz, mengatur System clock ke 72 MHz, mengatur Bus clock ke 72 MHz, dan mengatur ADC clock ke 9 MHz.

1let mut gpioa = dp.GPIOA.split(&mut clocks);
2let potentio = gpioa.pa0.into_analog(&mut gpioa.crl);

Mengakses periferal GPIO A dan mengonfigurasi pin PA0 sebagai input analog yang dihubungkan ke potensiometer. Sesuai dengan gambar skematik sebelumnya.

1let mut adc1 = adc::Adc::new(dp.ADC1, &mut clocks);
2
3adc1.set_sample_time(adc::SampleTime::T_239);

Mengakses periferal ADC1. Kemudian mengatur sample time ADC1 ke 239 cycles.

1let dma1_channels = dp.DMA1.split(&mut clocks);

Mengakses periferal DMA1.

1let adc_dma = adc1.with_dma(potentio, dma1_channels.1);

Mengonfiguras ADC1 untuk menggunakan DMA1 channel 1 dan secara otomatis akan mengubah ADC1 ke mode continuous conversion.

Catatan: Pada STM32F103C8 hanya DMA channel 1 yang bisa digunakan untuk ADC, jika anda menggunakan channel lain maka akan gagal. Pembahasan mengenai Direct Memory Access (DMA) akan dibahas pada artikel selanjutnya.

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

Menyiapkan buffer yang akan diisi nilai ADC oleh DMA. Kita menggunakan Array unsigned integer 16 bit dengan panjang buffer 300 item sebanyak 2 buah untuk setengah buffer bagian pertama dan setengah buffer bagian kedua.

1let mut circ_buffer = adc_dma.circ_read(buffer);

Memerintahkan ADC1 untuk memulai konversi. Kita hanya perlu memerintahkan konversi sekali saja karena sudah dalam mode continuous conversion, sehingga selanjutnya ADC1 akan otomatis akan melakukan konversi lagi secara terus menerus tanpa menunggu perintah dari CPU lagi. ADC1 dibantu DMA akan otomatis mengisi setengah buffer (pertama atau kedua) setiap selesai melakukan konversi.

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 circular read karena akan mempengaruhi waktu pembacaan ke buffer DMA, sehingga rawan terjadi eror overrun .

Di dalam blok loop, kita akan mengisinya program utama kita.

1loop {
2    if let Ok(half) = circ_buffer.readable_half() {
3        // program membaca nilai ADC
4    }
5}

Di dalam loop kita akan mengecek circular buffer , jika sudah bisa dibaca setengah buffernya maka kita akan mengambil nilainya.

Di dalam blok if tambahkan kode berikut:

1let avg = circ_buffer.peek(|data, h| {
2    if h == half {
3        let sum: u32 = data.iter().map(|&v| v as u32).sum();
4        (sum / data.len() as u32) as u16
5    } else {
6        0
7    }
8});

Kita menggunakan method peek untuk membaca setengah buffer yang sudah terisi dari circular_buffer . Jika nilai dari variabel h sama dengan variabel half pada readable_half() maka hitung rata-ratanya dan simpan di variabel avg . Atau jika tidak sama maka simpan nilai 0 di variabel avg .

 1let now = counter.now();
 2
 3if last_update_time.ticks().wrapping_sub(now.ticks()) >= interval.ticks() {
 4    if let Ok(avg) = avg {
 5        let voltage = (avg as f32 / 4095.0) * 3.3;
 6        defmt::println!("ADC: {} | Volt: {}V", avg, voltage);
 7    }
 8
 9    last_update_time = now;
10}

Pertama kita simpan nilai counter saat ini ke variabel now . Kemudian kurangkan variabel last_update_time dengan now , jika lebih besar dari interval maka hitung nilai voltage dari nilai ADC dan kirim 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 .

Catatan: Disini kami menggunakan wrapping_sub(now.ticks()) artinya last_update_time dikurangi now karena counter System Timer bersifat count down .

Tampilkan kode full: Mode Continuous Conversion DMA
 1#![no_std]
 2#![no_main]
 3
 4use cortex_m_rt::entry;
 5
 6use defmt_rtt as _;
 7use panic_probe as _;
 8use stm32f1xx_hal::{
 9    adc::{self},
10    dma::{CircReadDma, DmaExt},
11    flash::FlashExt,
12    gpio::GpioExt,
13    pac,
14    prelude::*,
15    rcc::{Config, RccExt},
16    time::Hertz,
17    timer::SysTimerExt,
18};
19
20#[entry]
21fn main() -> ! {
22    defmt::println!("Input Analog Mode Continuous DMA");
23
24    let dp = pac::Peripherals::take().unwrap();
25
26    let cp = pac::CorePeripherals::take().unwrap();
27
28    let mut flash = dp.FLASH.constrain();
29    let rcc = dp.RCC.constrain();
30
31    let clock_cfgr = Config::default()
32        .use_hse(Hertz::MHz(8))
33        .sysclk(Hertz::MHz(72))
34        .hclk(Hertz::MHz(72))
35        .adcclk(Hertz::MHz(9));
36
37    let mut clocks = rcc.freeze(clock_cfgr, &mut flash.acr);
38
39    let mut gpioa = dp.GPIOA.split(&mut clocks);
40    let potentio = gpioa.pa0.into_analog(&mut gpioa.crl);
41
42    let mut adc1 = adc::Adc::new(dp.ADC1, &mut clocks);
43
44    adc1.set_sample_time(adc::SampleTime::T_239);
45
46    let dma1_channels = dp.DMA1.split(&mut clocks);
47
48    let adc_dma = adc1.with_dma(potentio, dma1_channels.1);
49
50    let buffer = cortex_m::singleton!(: [[u16; 300];2] = [[0; 300];2]).unwrap();
51
52    let mut circ_buffer = adc_dma.circ_read(buffer);
53
54    let mut counter = cp.SYST.counter_us(&clocks.clocks);
55    let _ = counter.start(230_000.micros()).unwrap();
56    let mut last_update_time = counter.now();
57    let interval = 200_000u32.micros::<1, 1_000_000>();
58
59    loop {
60        if let Ok(half) = circ_buffer.readable_half() {
61            let avg = circ_buffer.peek(|data, h| {
62                if h == half {
63                    let sum: u32 = data.iter().map(|&v| v as u32).sum();
64                    (sum / data.len() as u32) as u16
65                } else {
66                    0
67                }
68            });
69
70            let now = counter.now();
71
72            if last_update_time.ticks().wrapping_sub(now.ticks()) >= interval.ticks() {
73                if let Ok(avg) = avg {
74                    let voltage = (avg as f32 / 4095.0) * 3.3;
75                    defmt::println!("ADC: {} | Volt: {}V", avg, voltage);
76                }
77
78                last_update_time = now;
79            }
80        }
81    }
82}

Jalankan program dengan perintah ‘cargo run --bin continuous-conversion-dma’ pada terminal VSCode. Berikut merupakan hasilnya:

Memprogram Blue Pill (STM32F103C8) Sebagai Input Analog dengan Continuous Conversion Interrupt

Karena crate stm32f1xx_hal tidak menyediakan method untuk mengakses ADC dengan mode continuous conversion interrupt, maka kita akan melakukannya dengan mengakses langsung ke register periferal ADC mikrokontroler STM32F103C8.

Definisikan binary executable baru dengan nama continuous-conversion-interrupt pada file Cargo.toml:

1[[bin]]
2name = "continuous-conversion-interrupt"
3path = "src/continuous_conversion_interrupt.rs"
4test = false
5bench = false

Selanjutnya buat file src/continuous_conversion_interrupt.rs dan tambahkan kode berikut untuk mendefinisikan libray dan modul yang digunakan:

 1#![no_std]
 2#![no_main]
 3
 4use core::sync::atomic::{AtomicI16, Ordering};
 5
 6use cortex_m_rt::entry;
 7
 8use defmt_rtt as _;
 9use panic_probe as _;
10use stm32f1xx_hal::{
11    adc::{self, ChannelTimeSequence},
12    flash::FlashExt,
13    gpio::{GpioExt, PinExt},
14    hal::delay::DelayNs,
15    pac::{self, interrupt},
16    rcc::{Config, RccExt},
17    time::Hertz,
18    timer::SysTimerExt,
19};

Disana kita menggunakan library atomic untuk menyimpan data pada variabel yang dibagikan antar thread secara aman, dalam hal ini agar dapat diakses dari fungsi Interupsi dan fungsi utama.

1static ADC_VALUE: AtomicI16 = AtomicI16::new(0);

Membuat variabel global dengan tipe Atomic16 untuk menyimpan nilai ADC agar dapat diakses dengan aman baik dari fungsi Interupsi dan fungsi utama.

Pada fungsi main tambahkan kode berikut:

1defmt::println!("Input Analog Mode Continuous Interrupt");
2
3let dp = pac::Peripherals::take().unwrap();
4let cp = pac::CorePeripherals::take().unwrap();

Berfungsi untuk mengirim pesan ke PC/laptop, kemudian mengakses periferal STM32F103C8 dan periferal CPU Cortex-M3.

 1let mut flash = dp.FLASH.constrain();
 2let rcc = dp.RCC.constrain();
 3
 4let clock_cfgr = Config::default()
 5        .use_hse(Hertz::MHz(8))
 6        .sysclk(Hertz::MHz(72))
 7        .hclk(Hertz::MHz(72))
 8        .adcclk(Hertz::MHz(9));
 9
10let mut clocks = rcc.freeze(clock_cfgr, &mut flash.acr);

Mengakses periferal flash dan periferal reset & control clock (RCC). Kemudian mengonfigurasi clock ke clock eksternal sebesar 8 MHz, mengatur System clock ke 72 MHz, mengatur Bus clock ke 72 MHz, dan mengatur ADC clock ke 9 MHz.

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

Membuat instance delay untuk memberikan waktu jeda.

1let mut gpioa = dp.GPIOA.split(&mut clocks);
2let potentio = gpioa.pa0.into_analog(&mut gpioa.crl);

Mengakses periferal GPIO pin PA0 sebagai input analog yang dihubungkan ke potensiometer.

1let mut adc1 = adc::Adc::new(dp.ADC1, &mut clocks);
2
3adc1.set_sample_time(adc::SampleTime::T_239);
4adc1.set_continuous_mode(true);
5adc1.enable_eoc_interrupt();

Mengakses periferal ADC1. Kemudian mengonfigurasi sample time ADC1 ke 239 cycle dengan mode continuous conversion. Kita juga mengaktifkan EOC interrupt pada ADC1.

1unsafe {
2    pac::NVIC::unmask(interrupt::ADC1_2);
3}

Mengaktifkan interrupt ADC1_2 agar CPU ARM Cortex-M3 dapat menerima interrupt dari ADC1.

1let adc_reg = unsafe { &*pac::ADC1::ptr() };

Mengakses pointer register ADC1 secara langsung agar kita bisa mengubah bit registernya untuk menyesuaikan ke mode continuous conversion dengan metode poll.

1adc_reg.cr2().modify(|_, w| w.adon().clear_bit());

Mengatur register CR2 bit ADON ke 0 untuk mematikan ADC agar aman saat mengonfigurasi bit register yang lain.

1adc_reg.cr1().modify(|_, w| w.scan().clear_bit());

Menonaktifkan Mode Scan Conversion, karena kita hanya akan menggunakan 1 channel. Register CR1 bit SCAN akan bernilai 0.

1adc_reg.cr1().modify(|_, w| w.discen().clear_bit());

Menonaktifkan mode Discontinuous Conversion agar mode continuous conversion dapat berjalan. Mode Discontinuous Conversion otomatis diaktifkan ketika kita memanggil fungsi Adc::new . Register CR1 bit DISCEN akan bernilai 0.

1adc_reg.sqr1().modify(|_, w| unsafe { w.l().bits(0b0000) });

Menentukan jumlah channel yang digunakan, karena kita menggunakan 1 channel maka diisi dengan 0 (jumlah channel-1).

1adc_reg.cr2().modify(|_, w| w.adon().set_bit());

Menyalakan kembali ADC1. Register CR2 bit ADON akan bernilai 1.

1adc_reg
2    .sqr3()
3    .modify(|_, w| unsafe { w.sq1().bits(potentio.pin_id()) });

Mengatur channel ADC yang akan dibaca. Perintah potentio.pin_id() akan mengembalikan nilai pin ID dari pin PA0 yaitu 0.

1// adc_reg.cr2().modify(|_, w| w.adon().set_bit());
2adc_reg.cr2().modify(|_, w| w.swstart().set_bit());

Memulai konversi dengan mengatur register CR2 bit SWSTART ke 1. Kita juga bisa memulai konversi dengan mengatur register CR2 bit ADON ke 1 sekali lagi. Silakan pilih salah satu sesuai dengan keinginan anda.

Di dalam blok loop kita isi dengan kode berikut:

 1defmt::println!("Main program");
 2
 3// Load value from global variable ADC_VALUE and calculate voltage
 4let val = ADC_VALUE.load(Ordering::Relaxed);
 5let voltage = (val as f32 / 4095.0) * 3.3;
 6
 7// Send results to PC/laptop
 8defmt::println!("Raw Avg: {} | Volt: {}V", val, voltage);
 9
10// Wait for 200ms
11delay.delay_ms(200);

Pertama kita mengirimkan pesan ke terminal PC/laptop untuk menandai ini sebagai program utama. Kemudian membaca nilai ADC dari variabel global ADC_VALUE yang selanjutnya dihitung untuk mendapatkan tegangan input dari potensiometer. Selanjutnya nilai ADC dan tegangan dikirimkan ke terminal PC/laptop, dan kita beri jeda selama 200 ms.

Selanjutnya buat fungsi baru bernama ADC1_2 yang menerapkan macro interrupt .

 1#[interrupt]
 2fn ADC1_2() {
 3    // Send message to PC/laptop
 4    defmt::println!("Interrupt ADC");
 5
 6    // Access the ADC1 register pointer directly
 7    let adc_reg = unsafe { &*pac::ADC1::ptr() };
 8
 9    // Check if ADC1 triggered the interrupt due to EOC (End of Conversion) or other factors.
10    if adc_reg.sr().read().eoc().bit_is_set() {
11        // Read value from ADC1 Data Register (DR)
12        let val = adc_reg.dr().read().bits();
13        // Store the ADC value into global variable ADC_VALUE
14        ADC_VALUE.store(val as i16, Ordering::Relaxed);
15
16        defmt::println!("ADC Value: {}", val);
17    }
18}

Fungsi ini akan dieksekusi oleh CPU ketika ADC1 mengirimkan interupsi ke CPU. Pertama kita mengirimkan pesan ke terminal PC/laptop untuk menandai bahwa ini program interupsi. Kemudian mengakses pointer register ADC1. Selanjutnya mengecek nilai bit EOC (End of Conversion) pada register SR, Jika nilainya 1 maka baca nilai ADC dari DATA di register DR yang selanjutnya disimpan di global variabel ADC_VALUE . Selain itu kita juga mengirimkan nilai ADC ke terminal PC/laptop.

Tampilkan kode full: Mode Continuous Conversion Interrupt
  1#![no_std]
  2#![no_main]
  3
  4use core::sync::atomic::{AtomicI16, Ordering};
  5
  6use cortex_m_rt::entry;
  7
  8use defmt_rtt as _;
  9use panic_probe as _;
 10use stm32f1xx_hal::{
 11    adc::{self, ChannelTimeSequence},
 12    flash::FlashExt,
 13    gpio::{GpioExt, PinExt},
 14    hal::delay::DelayNs,
 15    pac::{self, interrupt},
 16    rcc::{Config, RccExt},
 17    time::Hertz,
 18    timer::SysTimerExt,
 19};
 20
 21static ADC_VALUE: AtomicI16 = AtomicI16::new(0);
 22
 23#[entry]
 24fn main() -> ! {
 25    defmt::println!("Input Analog Mode Continuous Interrupt");
 26
 27    let dp = pac::Peripherals::take().unwrap();
 28
 29    let cp = pac::CorePeripherals::take().unwrap();
 30
 31    let mut flash = dp.FLASH.constrain();
 32    let rcc = dp.RCC.constrain();
 33
 34    let clock_cfgr = Config::default()
 35        .use_hse(Hertz::MHz(8))
 36        .sysclk(Hertz::MHz(72))
 37        .hclk(Hertz::MHz(72))
 38        .adcclk(Hertz::MHz(9));
 39
 40    let mut clocks = rcc.freeze(clock_cfgr, &mut flash.acr);
 41
 42    let mut delay = cp.SYST.delay(&clocks.clocks);
 43
 44    let mut gpioa = dp.GPIOA.split(&mut clocks);
 45    let potentio = gpioa.pa0.into_analog(&mut gpioa.crl);
 46
 47    let mut adc1 = adc::Adc::new(dp.ADC1, &mut clocks);
 48
 49    adc1.set_sample_time(adc::SampleTime::T_239);
 50    adc1.set_continuous_mode(true);
 51    adc1.enable_eoc_interrupt();
 52
 53    unsafe {
 54        pac::NVIC::unmask(interrupt::ADC1_2);
 55    }
 56
 57    let adc_reg = unsafe { &*pac::ADC1::ptr() };
 58
 59    adc_reg.cr2().modify(|_, w| w.adon().clear_bit());
 60
 61    adc_reg.cr1().modify(|_, w| w.scan().clear_bit());
 62
 63    adc_reg.cr1().modify(|_, w| w.discen().clear_bit());
 64
 65    adc_reg.sqr1().modify(|_, w| unsafe { w.l().bits(0b0000) });
 66
 67    adc_reg.cr2().modify(|_, w| w.adon().set_bit());
 68
 69    adc_reg
 70        .sqr3()
 71        .modify(|_, w| unsafe { w.sq1().bits(potentio.pin_id()) });
 72
 73    // adc_reg.cr2().modify(|_, w| w.adon().set_bit());
 74    adc_reg.cr2().modify(|_, w| w.swstart().set_bit());
 75
 76    loop {
 77        // Send message to PC/laptop
 78        defmt::println!("Main program");
 79
 80        // Load value from global variable ADC_VALUE and calculate voltage
 81        let val = ADC_VALUE.load(Ordering::Relaxed);
 82        let voltage = (val as f32 / 4095.0) * 3.3;
 83
 84        // Send results to PC/laptop
 85        defmt::println!("Raw Avg: {} | Volt: {}V", val, voltage);
 86
 87        // Wait for 200ms
 88        delay.delay_ms(200);
 89    }
 90}
 91
 92#[interrupt]
 93fn ADC1_2() {
 94    // Send message to PC/laptop
 95    defmt::println!("Interrupt ADC");
 96
 97    // Access the ADC1 register pointer directly
 98    let adc_reg = unsafe { &*pac::ADC1::ptr() };
 99
100    // Check if ADC1 triggered the interrupt due to EOC (End of Conversion) or other factors.
101    if adc_reg.sr().read().eoc().bit_is_set() {
102        // Read value from ADC1 Data Register (DR)
103        let val = adc_reg.dr().read().bits();
104        // Store the ADC value into global variable ADC_VALUE
105        ADC_VALUE.store(val as i16, Ordering::Relaxed);
106
107        defmt::println!("ADC Value: {}", val);
108    }
109}

Jalankan program tersebut dengan perintah ‘cargo run --bin continuous-conversion-interrupt’ pada terminal VSCode. Berikut merupakan hasilnya:

Pada hasil tersebut dapat kita lihat bahwa CPU terus menerus diinterupsi oleh ADC. Sehingga mode ini bisa berbahaya karena CPU menjadi sibuk dan tidak sempat mengeksekusi program utama.

Permasalahan & Kesimpulan

Permasalahan yang kami temui ketika menggunakan mode continuous conversion adalah:

  • Pada metode continuous conversion poll, continuous conversion gagal: Hal ini terjadi karena pada saat kita memanggil fungsi Adc::new secara otomatis mengeset register CR1 bit DISCEN ke 1. Solusinya adalah dengan secara manual mengubah register CR1 bit DISCEN ke 0 seperti pada contoh diatas.

Jika ingin menggunakan mode continuous conversion pada Blue Pill (STM32F103C8) metode terbaik adalah dengan menggunakan metode continuous conversion DMA, karena sudah didukung langsung oleh crate stm32f1xx_hal , dan juga hemat CPU karena CPU hanya tinggal membaca data dari buffer.

Source code yang digunakan dalam tutorial ini dapat diakses di repository Github. Jika anda memiliki pertanyaan, kritik atau saran silakan hubungi kami melalui halaman kontak.