STM32 LibOpenCM3:ADC Timer 觸發

前言

ADC(Analog to Digital Converter)顧名思義是將類比訊號轉換成數位訊號的元件,現今多數 MCU 都會內建 ADC,而這也是相當基本且常用的功能。

之前的文章已經介紹過 3 中不同的 ADC 使用環境,這次要再介紹以 Timer 定期觸發 ADC 進行轉換的寫法,且一樣會啓用 ADC 的轉換完成(EOC)中斷。

正文

首先一樣以 Nucleo-F446RE 做示範。

首先建立一個 PIO 的專案,選擇 Framework 為「libopencm3」,並在 src/ 資料夾中新增並開啓 main.cmain.h

完整程式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/**
* @file main.c
* @brief ADC external trigger by timer example for
* STM32 Nucleo-F446RE.
*/

#include "main.h"

int main(void)
{
rcc_setup();
usart_setup();
adc_setup();
timer_setup();

while (1)
{ }
return 0;
}

static void rcc_setup(void)
{
rcc_clock_setup_pll(&rcc_hse_8mhz_3v3[RCC_CLOCK_3V3_84MHZ]);

rcc_periph_clock_enable(RCC_USART_TX_GPIO);
rcc_periph_clock_enable(RCC_USART2);
rcc_periph_clock_enable(RCC_ADC_GPIO);
rcc_periph_clock_enable(RCC_ADC1);
rcc_periph_clock_enable(RCC_TIM3);
rcc_periph_reset_pulse(RST_TIM3);
}

static void adc_setup(void)
{
/* Set to input analog. */
gpio_mode_setup(GPIO_ADC_PORT,
GPIO_MODE_ANALOG,
GPIO_PUPD_NONE,
GPIO_ADC_IN0_PIN);

uint32_t adc = ADC1;

/* Setup ADC. */
adc_power_off(adc);

adc_disable_scan_mode(adc);
adc_set_single_conversion_mode(adc);
adc_set_right_aligned(adc);
adc_set_sample_time_on_all_channels(adc, ADC_SIMPLE_TIME);

adc_enable_external_trigger_regular(adc,
ADC_CR2_EXTSEL_TIM3_TRGO,
ADC_CR2_EXTEN_RISING_EDGE);

/* Setup interrupt. */
adc_enable_eoc_interrupt(adc);
nvic_enable_irq(ADC_IRQ);

uint8_t channels[16];
channels[0] = 0;
adc_set_regular_sequence(adc, 1, channels);

adc_power_on(adc);
delay(800000); /* Wait a bit. */
}

static void timer_setup(void)
{
uint32_t timer = TIM3;

timer_set_mode(timer,
TIM_CR1_CKD_CK_INT,
TIM_CR1_CMS_EDGE,
TIM_CR1_DIR_UP);
timer_set_prescaler(timer, TIMER_PRESCALER); /* PSC. */
timer_set_period(timer, TIMER_PERIOD); /* ARR. */

/* The update event is selected as trigger output (TRGO). */
timer_set_master_mode(timer, TIM_CR2_MMS_UPDATE);

timer_enable_counter(timer);
}

static void usart_setup(void)
{
/* Set USART-Tx pin to alternate function. */
gpio_mode_setup(GPIO_USART_TX_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_USART_TX_PIN);
gpio_set_af(GPIO_USART_TX_PORT, GPIO_USART_AF, GPIO_USART_TX_PIN);

/* Config USART params. */
usart_set_baudrate(USART2, USART_BAUDRATE);
usart_set_databits(USART2, 8);
usart_set_stopbits(USART2, USART_STOPBITS_1);
usart_set_parity(USART2, USART_PARITY_NONE);
usart_set_flow_control(USART2, USART_FLOWCONTROL_NONE);
usart_set_mode(USART2, USART_MODE_TX);

usart_enable(USART2);
}

static void delay(uint32_t value)
{
for (uint32_t i = 0; i < value; i++)
{
__asm__("nop"); /* Do nothing. */
}
}

/* For printf(). */
int _write(int file, char *ptr, int len)
{
int i;

if (file == 1)
{
for (i = 0; i < len; i++)
{
usart_send_blocking(USART2, ptr[i]);
}
return i;
}

errno = EIO;
return -1;
}

/**
* @brief ADC Interrupt service routine.
*/
void adc_isr(void)
{
/* Clear regular end of conversion flag. */
ADC_SR(ADC1) &= ~ADC_SR_EOC;

uint16_t value = adc_read_regular(ADC1);
printf("%4d\r\n", value);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* @file main.h
*/

#ifndef MAIN_H
#define MAIN_H

#include <stdio.h> /* For printf(). */
#include <errno.h> /* For printf(). */
#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>
#include <libopencm3/stm32/adc.h>
#include <libopencm3/stm32/usart.h>
#include <libopencm3/stm32/timer.h>
#include <libopencm3/cm3/nvic.h>

#define USART_BAUDRATE (9600)

#define GOAL_FREQUENCY (1) /* in Hz. */
#define TIMER_CLOCK (rcc_apb1_frequency * 2)
#define COUNTER_CLOCK (10000)

/* PSC. */
#define TIMER_PRESCALER (TIMER_CLOCK / COUNTER_CLOCK - 1)

/* ARR. */
#define TIMER_PERIOD (((TIMER_CLOCK) / ((TIMER_PRESCALER + 1) * GOAL_FREQUENCY)) - 1)

#define ADC_SIMPLE_TIME (ADC_SMPR_SMP_56CYC)
#define RCC_ADC_GPIO (RCC_GPIOA)
#define GPIO_ADC_PORT (GPIOA)
#define GPIO_ADC_IN0_PIN (GPIO0) /* Arduino-A0. */
#define ADC_IRQ (NVIC_ADC_IRQ)

#define RCC_USART_TX_GPIO (RCC_GPIOA)
#define GPIO_USART_TX_PORT (GPIOA)
#define GPIO_USART_TX_PIN (GPIO2) /* ST-Link (Arduino-D1). */
#define GPIO_USART_AF (GPIO_AF7) /* Ref: Table-11 in DS10693. */

static void rcc_setup(void);
static void usart_setup(void);
static void adc_setup(void);
static void timer_setup(void);
static void delay(uint32_t value);

#endif /* MAIN_H. */

分段說明

Include

1
2
3
4
5
6
7
8
9
// main.h
#include <stdio.h> /* For printf(). */
#include <errno.h> /* For printf(). */
#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>
#include <libopencm3/stm32/adc.h>
#include <libopencm3/stm32/usart.h>
#include <libopencm3/stm32/timer.h>
#include <libopencm3/cm3/nvic.h>

除了基本的 rcc.hgpio.h 及必要的 adc.h 外,因為我要使用 USART 和 printf(),所以還會需要 usart.hstdio.herrno.h

另外就是因為要使用中斷及 Timer,所以 nvic.htimer.h 也是必要的。

USART 和 printf() 的詳細用法請看之前的文章

設定 ADC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static void adc_setup(void)
{
/* Set to input analog. */
gpio_mode_setup(GPIO_ADC_PORT,
GPIO_MODE_ANALOG,
GPIO_PUPD_NONE,
GPIO_ADC_IN0_PIN);

uint32_t adc = ADC1;

/* Setup ADC. */
adc_power_off(adc);

adc_disable_scan_mode(adc);
adc_set_single_conversion_mode(adc);
adc_set_right_aligned(adc);
adc_set_sample_time_on_all_channels(adc, ADC_SIMPLE_TIME);

adc_enable_external_trigger_regular(adc,
ADC_CR2_EXTSEL_TIM3_TRGO,
ADC_CR2_EXTEN_RISING_EDGE);

/* Setup interrupt. */
adc_enable_eoc_interrupt(adc);
nvic_enable_irq(ADC_IRQ);

uint8_t channels[16];
channels[0] = 0;
adc_set_regular_sequence(adc, 1, channels);

adc_power_on(adc);
delay(800000); /* Wait a bit. */
}

要使用 ADC 功能,首先要知道 ADC 的通道在哪些 GPIO 上,並將其設定為類比輸入。

接下來就是要設定 ADC。

  • adc_disable_scan_mode() 禁能多通道掃描模式,因為本範例只需要讀取一個通道而已。
  • adc_set_single_conversion_mode() 設定成單一轉換模式,不連續轉換。
  • adc_set_right_aligned() 讓資料的對齊方式為靠右對齊。
  • adc_set_sample_time_on_all_channels() 設定所有通道的取樣時間,這裡使用 56 個 Cycle。
  • adc_enable_external_trigger_regular() 啓用 ADC 的外部觸發,並指定觸發源為 Timer3 的 TRGO(Tregger output)。
  • adc_enable_eoc_interrupt() 啓用 ADC 的轉換完成(EOC)中斷。
  • nvic_enable_irq() 啓用 NVIC 的 ADC IRQ。
  • adc_set_regular_sequence() 設定 Regular 的通道序列。這裡只有 Ch0。

ADC ISQ

1
2
3
4
5
6
7
8
9
10
11
/**
* @brief ADC Interrupt service routine.
*/
void adc_isr(void)
{
/* Clear regular end of conversion flag. */
ADC_SR(ADC1) &= ~ADC_SR_EOC;

uint16_t value = adc_read_regular(ADC1);
printf("%4d\r\n", value);
}

這是 ADC 的 ISQ。

首先先清除 ADC 的轉換完成位元(EOC),再使用 adc_read_regular() 讀取 ADC 轉換完成的數值。

Timer 設定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void timer_setup(void)
{
uint32_t timer = TIM3;

timer_set_mode(timer,
TIM_CR1_CKD_CK_INT,
TIM_CR1_CMS_EDGE,
TIM_CR1_DIR_UP);
timer_set_prescaler(timer, TIMER_PRESCALER); /* PSC. */
timer_set_period(timer, TIMER_PERIOD); /* ARR. */

/* The update event is selected as trigger output (TRGO). */
timer_set_master_mode(timer, TIM_CR2_MMS_UPDATE);

timer_enable_counter(timer);
}

先設定好 Timer 的頻率(PSC 與 ARR)。

使用 timer_set_master_mode() 設定 Timer 在每次的 Update 事件都會產生 TRGO 訊號,以觸發 ADC。

Timer 的頻率設定請看之前的文章

多環境程式(F446RE + F103RB)

由於 STM32F1 的部分函式不同,所以 F103RB 沒辦法直接使用上面的 F446RE 的程式。

由於本例的差異比較大,為了不佔版面這裡就不列出的,完整的程式請看 GitHub repo

小結

若需要定期進行 ADC 轉換的話,使用 Timer 進行觸發是一個不錯的做法。本次範例使用 Timer 3 的 TRGO 訊號定期觸發 ADC 進行轉換,並且也有啓用 ADC 本身的轉換完成中斷,是一種比較有效率的寫法。

參考資料

本文的程式也有放在 GitHub 上。
本文同步發表於 iT 邦幫忙-2022 iThome 鐵人賽


留言可能不會立即顯示。若過了幾天仍未出現,請 Email 聯繫:)