STM32, примеры кода

Осваиваю периферию STM32F103C8, пишу код с комментами, подумал что может кому-то пригодиться (или мне самому по-позже). По ходу обучения буду дополнять эту статью.

Считываю данные с температурного датчика внутри чипа и передаю по USART. Используется таймер и прерывания. Прерывания убраны, добавлен DMA.

Step 1
фунция инициализации ЮАРТа
void UART_init(void)
{
	// тактируюсь от кварца на 12 МГц

	// включаю тактирование ЮАРТ1 и порта на которам он висит
	RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
	RCC->APB2ENR|= RCC_APB2ENR_AFIOEN;
	RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
		
	// настраиваю соответсв. порты на вход и выход от расшир. функций
	GPIOA->CRH &= ~GPIO_CRH_MODE9;
	GPIOA->CRH &= ~GPIO_CRH_CNF9;
	GPIOA->CRH &= ~GPIO_CRH_MODE10;
	GPIOA->CRH &= ~GPIO_CRH_CNF10;

	GPIOA->CRH |= GPIO_CRH_MODE9_1;	// выход на 2 МГц
	GPIOA->CRH |= GPIO_CRH_CNF9_1;

	GPIOA->CRH |= GPIO_CRH_CNF10_0;

	// 12 МГц/16*115200 = 6.5
	USART1->BRR = (6 << 4) + 8;
	// включаю ЮАРТ
	USART1->CR1 |= USART_CR1_UE;
	USART1->CR1 |= USART_CR1_TE;	// включил передатчик
	USART1->CR1 |= USART_CR1_RE;	// включил приемник
}


инициализация АЦП
void ADC_init(void)
{
	RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;	// подаем такты на АЦП 
	ADC1->CR2 |= ADC_CR2_ADON;		// подаем питание на ФЦП
	ADC1->CR2 |= ADC_CR2_TSVREFE;	// подаем питание на темп. сенсор и датчик напр.
	ADC1->CR2 |= ADC_CR2_EXTSEL;	// запуск преобразования по установки бита swstart	
	ADC1->CR2 |= ADC_CR2_EXTTRIG;	// включаем запуск от внешнего события (у нас это свтарт)
	ADC1->SMPR1 |= ADC_SMPR1_SMP16;	// ставлю макс. кол-во цыклов (239.5) на преобразование для 16 канала где теп. сенсор
	ADC1->SQR3 = ADC_SQR3_SQ1_4;	// выбираем 16 (0b10000) канал для 1 преобразования	(кол-во преобразования по-умолчанию 1)
}


ну и главный цикл
while(1)
	{
	ADC1->CR2 |= ADC_CR2_SWSTART;	
	//ADC1->CR2 |= ADC_CR2_ADON;	// запуск преобразования
	// ждем конца преобразования
	while (!(ADC1->SR & ADC_SR_EOC))
		;
	adc = ADC1->DR >> 4;	// считываем данные
	USART1->DR = adc;		// передаем
	GPIOA->BSRR =GPIO_BSRR_BS1;
//	if (USART1->SR & USART_SR_RXNE) {
//		USART1->DR = USART1->DR;
//		GPIOA->BSRR =GPIO_BSRR_BS1;
//		}

	for (i = 2000000; i > 0; i--)
		;
	}


Step 2
Добавлены прерывания, таймер.

Инициализация таймера, счет на увеличение до определенного значения, прерывание по достижению этого значения.
// использую таймер2, частота 12МГц, прерывание черз 1 с.
void TIM_init(void)
{
	RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;	// уже привычное, ключаю тактирование модуля
	//TIM2->PSC = 182; 	// пределитель 12000000/65536 - 1
	// так точнее будет
	TIM2->ARR = 60000;		// макс. значение до которого считаем
	TIM2->PSC = 199;		// пределитель 12000000/60000 - 1
	TIM2->CR1 |= TIM_CR1_CEN;	// запускаю счет
	TIM2->DIER |= TIM_DIER_UIE;	// прерывание по обновлению	
}


Разрешение прерываний и главная функция
int main(void)
{

UART_init();
ADC_init();
TIM_init();

__enable_irq();		// глобальное включение прерывания
NVIC_EnableIRQ(TIM2_IRQn);		// прерывание по таймеру2
NVIC_SetPriority(TIM2_IRQn, 1);	// приоритет немного ниже чем у юарта, т.к. он быстрее обрабатывается
NVIC_EnableIRQ(USART1_IRQn);	// прерывание от ЮАРТ, поумолчанию приоритет 0, самый высокий
USART1->CR1 |= USART_CR1_RXNEIE;	// прерывание по приему

while(1)
	{
		;
	}				
 
}


Добавил эхо-ответ ЮАРТА на прерывании
// т.к. разрешины прерывания только по приему то определять причину прерывания излишне
void USART1_IRQHandler (void)
{
	USART1->DR = USART1->DR;	// эхо по приему, флаг прерывание автоматически сбрасывается после чтения
}


Обработчик прерывания таймера
void TIM2_IRQHandler (void)
{
	TIM2->SR &= ~TIM_SR_UIF;	// очищаем флаг прерывания
	ADC1->CR2 |= ADC_CR2_SWSTART;	// запуск преобразования	
	// ждем конца преобразования 
        // это плохой пример, не делайте так, прерывания должны обрабатываться быстро, а здесь процессор висит до конца преобразования
	while (!(ADC1->SR & ADC_SR_EOC))
		;
	//adc = ADC1->DR;// >> 4;	// считываем данные
	USART1->DR = ADC1->DR;		// передаем
	GPIOA->BSRR =GPIO_BSRR_BS1;	
}


Step 3
Теперь АЦП запускается автоматически по сравнению таймера, с этим дольше всего мучался. Как в документации описано со вторым таймером не заработало, получилось только с первым, причем для чего-то надо запускать ШИМ. Ссылки по теме
electronix.ru/forum/
my.st.com/public/
www.stm32circle.com/forum/
Если кто знает как заставить работать со вторым таймером, поделитесь, буду очень признателен.

Теперь использую таймер1, вот его инициализация
void TIM_init(void)
{
	RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;	// уже привычное, ключаю тактирование модуля
	TIM1->ARR = 60000;		// макс. значение до которого считаем
	TIM1->PSC = 199;		// пределитель 12000000/60000 - 1
	TIM1->CCR1 = 30000;			// любое число от 0 до TIM1->ARR

	// зачем это не знаю, но без этого не работает
	TIM1->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2;	// включаю ШИМ
	TIM1->CR2 |= TIM_CR2_OIS1;

	TIM1->CR1 |= TIM_CR1_CEN;	// запускаю счет
	//TIM2->DIER |= TIM_DIER_UIE;	// прерывание по обновлению	
}


Инициализаци АЦП тоже немного изменилась
void ADC_init(void)
{
	RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;	// подаем такты на АЦП 
	ADC1->CR2 |= ADC_CR2_ADON;		// подаем питание на АЦП
	ADC1->CR2 |= ADC_CR2_TSVREFE;	// подаем питание на темп. сенсор и датчик напр.
	ADC1->CR2 &= ~ADC_CR2_EXTSEL;	// запуск преобразования по совпадинию1 таймера1
	ADC1->CR2 |= ADC_CR2_EXTTRIG;	// включаем запуск от внешнего события
	ADC1->SMPR1 |= ADC_SMPR1_SMP16;	// ставлю макс. кол-во цыклов (239.5) на преобразование для 16 канала где теп. сенсор
	ADC1->SQR3 = ADC_SQR3_SQ1_4;	// выбираем 16 (0b10000) канал для 1 преобразования	(кол-во преобразования по-умолчанию 1)
	ADC1->CR1 |= ADC_CR1_EOCIE;		// вкл. прерывания
}


Ну и правильное прерывание по окончанию преобразования АЦП
void ADC1_2_IRQHandler (void)
{
	USART1->DR = ADC1->DR;		// передаем
	GPIOA->BSRR =GPIO_BSRR_BS1;
}


Step 4
Добавил DMA, теперь таймер каждую секунду запускает преобразование АЦП, после окончания данные передаются с помощью DMA в буфер USART.

Инициализация DMA
// будет передавать данные с АЦП в ЮАРТ по инициативе АЦП, использую 1 канал ПДП
void DMA_init (void)
{
	RCC->AHBENR |= RCC_AHBENR_DMA1EN;	// подаю такты на DMA1
	DMA1_Channel1->CPAR = (uint32_t) &ADC1->DR;	// адрес периферии
	DMA1_Channel1->CMAR = (uint32_t) &USART1->DR;	// адрес памяти
	DMA1_Channel1->CNDTR = 1;	// количество передач данных, у меня цыклическая передача, счетчик после достижения 0 будет обновляться
// так получилось что значения по умолчанию меня устраивают (т.е. чтение из периферии, 8 бит, без инкримента и т.д.)
	DMA1_Channel1->CCR |= DMA_CCR1_CIRC; 	// цыклическая передача
	DMA1_Channel1->CCR |= DMA_CCR1_EN;		// включаем	
}


В инициализации АЦП небольшие изменения
ADC1->CR2 |= ADC_CR2_DMA;		// включаю DMA
//ADC1->CR1 |= ADC_CR1_EOCIE;		// вкл. прерывания


Вот и все, периферия работает сама, а ядро тупит в цикле
int main(void)
{

UART_init();
ADC_init();
TIM_init();
DMA_init();

__enable_irq();		// глобальное включение прерывания

NVIC_EnableIRQ(USART1_IRQn);	// прерывание от ЮАРТ, поумолчанию приоритет 0, самый высокий
USART1->CR1 |= USART_CR1_RXNEIE;	// прерывание по приему

while(1)
	{
		;
	}				
 
}


Пока пойду отдохну и подумаю чем занять Cortex-M3 с частотой 72 МГц :)

Будут вопросы пишите в комментах, по возможности отвечу.
Файлы в топике: main.zip

Комментарии (21)

RSS свернуть / развернуть
Перебрось лучше в коллективный STM32 примерчик простой и понятный. Там уместней эта инфа будет.
0
Осваиваю периферию STM32F103C8, пишу код с комментами, подумал что может кому-то пригодиться (или мне самому по-позже).
Конечно пригодится
0
Тогда буду пополнять эту статью. Чтобы удобнее было следить буду добавлять комментарий с кратким описанием. В приложении к статье мой текущий main.c
0
Добавил прерывания и таймер
0
> Считываю данные с температурного датчика внутри чипа
Расскажи, что там с начальными смещениями и точностью.
0
С точностью там походу вообще никак. Они сами не рекомендуют его использовать для измерения температуры, только для определения изменения.

В характеристиках датчика есть интересное примечание «Based on characterization, not tested in production.», думаю если определить калибровочные константы для конкретного чипа то его можно использовать для измерения с точностью в пару градусов.
0
DS я сам читал. :(
Average slope 4.0 4.3 4.6 mV/°C
Получается, что кроме офигенного начального смещения, еще и наклон почти +-10%

> если определить калибровочные константы для конкретного чипа
Вот уж нах@й. :) Тем более непонятно как Average slope и Voltage at 25°C поведут себя во времени. Проще, если необходимо, что нибудь от Далласа прикрутить.
0
void TIM2_IRQHandler (void)
{
        TIM2->SR &= ~TIM_SR_UIF;        // очищаем флаг прерывания
// Он не сам сбрасывается при переходе на обработчик? 


        ADC1->CR2 |= ADC_CR2_SWSTART;   // запуск преобразования        
        // ждем конца преобразования

// Шо это за быдлоциклы ожидания в обработчике прерывания? ;))))
        while (!(ADC1->SR & ADC_SR_EOC))
                ;
        //adc = ADC1->DR;// >> 4;       // считываем данные
        USART1->DR = ADC1->DR;          // передаем
        GPIOA->BSRR =GPIO_BSRR_BS1;     
}
0
Он не сам сбрасывается при переходе на обработчик?
Нет, там много событий может вызвать обработчик, даже самому надо думать что стало причиной прерывания.

Шо это за быдлоциклы ожидания в обработчике прерывания
в процессе обучения и не такое бывает) главное что прерывание заработало :)

сейчас нормальный код выложу
0
Т.е. у таймера всего одно прерывание на все события. А не пучек, как у AVR?

Для обучения лучше сразу писать более менее правильно. По крайней мере без явного быдляка. Т.к. такие примеры потом в базу закладываются.
0
Это еще зависит от таймера. У второго таймера одно прерывание на все события, а у первого они разделены по группам: одно прерывание на событие сравнения (а таких регистров сравнения 4) и отдельное прерывание по переполнению (точнее здесь оно обновление) и еще 2 прерывания.

И еще. Когда срабатывает прерывание по приему ЮАРТа или завершения преобразования АЦП флаг сбрасывается не при переходе по вектору а при чтении из регистра данных.
0

        ADC1->CR2 |= ADC_CR2_TSVREFE;   // подаем питание на темп. сенсор и датчик напр.
        ADC1->CR2 &= ~ADC_CR2_EXTSEL;   // запуск преобразования по совпадинию1 таймера1


Извиняюсь за «нубский» вопрос, но вторая строчка вызывает непонимание. Насколько я понял из кода, ADC1->CR2 — это битовая маска. В первой строчке приведенного мной кода, мы выставляем в ней бит ADC_CR2_TSVREFE. Все остальные биты в CR2 остаются прежними, т.к. стоит ИЛИ.

Теперь вторая строка. Там стоит И и отрицание. Что это? Зачем? Вся маска сотрется? Допустим, маска была 0110, а ADC_CR2_EXTSEL равен 1110. Отрицаем его, получаем 0001. Выполняем операцию &= с маской, получаем:
0000

Я что-то не так понял?
0
Всё правильно.
VAL |= MASK; — устанавливает в VAL те биты, которые установлены в MASK, остальные не изменяет.
VAL &= ~MASK; — снимает в VAL те биты, которые установлены в MASK, остальные не изменяет.

В Вашем втором примере как раз были очищены биты с 3-го по 1-й, 0-й бит остался неизменным (0).
+1
Только ADC1->CR2 — битовое поле, а ADC_CR2_TSVREFE и ADC_CR2_EXTSEL — битовые маски.
+1
Допустим, маска была 0110, а ADC_CR2_EXTSEL равен 1110. Отрицаем его, получаем 0001
Тока отрицать надо маску. Т.е. 1001, накладываем на регистр и получаем 1000. А вообще стандартная операция, обнуление битов по маске.
+1
Понял, спасибо :)
0
Очень актуальная статья.
0
  • avatar
  • basil
  • 23 августа 2011, 18:36
А с какой скоростью будут гнаться данные из АЦП в УАРТ? И как у ДМА обстоит дело с битом TXE (Transmit data register empty) в регистре USART_SR? Данные будут кидаться в USART_DR тогда когда TXE=1?
Раз преобразование АЦП у вас выполняется по событию от таймера, то все остальное время ДМА непрерывно кидает в УАРТ данные последнего преобразования АЦП или при каждом событии таймера ДМА кинет лишь один байт из АЦП в УАРТ? Инициация циклической передачи в ДМА подсказывает что данные будут лететь в УАРТ постояно.
0
По большому счету ДМА пофиг на то что твориться в УАРТЕ, он по команде отправляет данные, никакого контроля TXE не производится. Чтобы это учитывалось нужно настраивать УАРТ на запуск ДМА когда регистр пуст, тогда он будет гнать непрерывно данные.

В моем примере таймер запускает АЦП преобразование, по окончанию оцифровки АЦП запускает передачу байта, т.е. по одной передаче на 1 преобразование. Частота оцифровки много меньше скорости передачи, все успевает.
0
Если кто знает как заставить работать со вторым таймером, поделитесь, буду очень признателен.

void TIM2_init(void)
 {
  RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;    
  TIM2->ARR = 16000;   
  TIM2->PSC = 1000;
  //TIM2->DIER |= TIM_DIER_UIE; 
  TIM2->CCR4  = 8000; 
  TIM2->CCMR1&=~TIM_CCMR1_CC2S; // capture/compare select - output compare
  TIM2->CCMR1&=~TIM_CCMR1_OC2M_0; // PWM mode on channel2
  TIM2->CCMR1|= TIM_CCMR1_OC2M_1;
  TIM2->CCMR1|= TIM_CCMR1_OC2M_2;
  TIM2->CCMR1|= TIM_CCMR1_OC2PE;
  TIM2->CCER |= TIM_CCER_CC2E; // c/c enable register: cc2e - enable , cc2p-polarity
  TIM2->CR1  |= TIM_CR1_CEN;   
  TIM2->CR1  |= TIM_CR1_ARPE; 
  }

Сначала потренировался на 4канале таймера4. АЦП затактировался от таймера и заработал, а при настройке PB9 на alternate push-pull output, на нем как и положено появился меандр.
Посмотрел, что у вас не работает с таймером2, добавил код для таймера2 — АЦП также продолжал работать, тактируясь уже от 2 канала таймера2 (изменить биты EXTSEL в ADC_CR2), но никакой гребенки на выводах PA1 и PB3 я получить не смог(. В чем может быть причина?
Сперва пытался вывести шим на PA1:
// Configuring PA1  
  RCC->APB2ENR|=RCC_APB2ENR_IOPAEN |RCC_APB2ENR_AFIOEN; //PA clock en, AFIO en
  GPIOA->CRL |= GPIO_CRL_MODE1 ;   // out50MHz
  GPIOA->CRL &= ~(GPIO_CRL_CNF1_0); // alternate push-pull output
  GPIOA->CRL |= GPIO_CRL_CNF1_1;

Не заработало. Затем аналогично для PB3:
// Configuring PB3 
  RCC->APB2ENR|=RCC_APB2ENR_IOPBEN |RCC_APB2ENR_AFIOEN; //PB clock en, AFIO en
  GPIOB->CRL |= GPIO_CRL_MODE3 ;   // out50MHz
  GPIOB->CRL &= ~(GPIO_CRL_CNF3_0); // alternate push-pull output
  GPIOB->CRL |= GPIO_CRL_CNF3_1;
  AFIO->MAPR |= AFIO_MAPR_TIM2_REMAP_0; // remap PA1 to PB3

Тоже нифига. В чем косяк может быть? USART2_RTS и канал1 АЦП ADC1_IN1, которые тоже висят на PA1, не задействованы.
0
Ну вот так всегда. Столько времени паришься, потом постишь сообщение и бага сразу находится:
было —
TIM2->CCR4  = 8000;

надо —
TIM2->CCR2  = 8000;

Конечно же, надо было заносить число сравнения во 2канал таймера, а не в 4ый. Раз там был ноль, вот и получалось что генерации не было.
Хотя, с другой стороны, почему АЦП тактировалось? Выходит все-таки получался маленький-маленький импульс.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.