[Программирование микроконтроллеров] Очередная статья: STM32 для начинающих
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Всех приветствую!
Это моя первая статья на хабре, поэтому прошу не кидаться тяжелыми предметами. Заранее спасибо.
Начнем с предыстории. Когда-то мне пришлось перейти на микроконтроллеры ARM фирмы ST. Это было связано с тем, что PIC и AVR уже не хватало и хотелось новых приключений. Из доступного в хлебобулочных магазинах и большого количества статей о «быстром старте» выбор пал именно на STM32F100.
Я привык работать в IAR. Да, есть другие IDE, но мне хватает возможности IAR: относительно удобный редактор, не плохой отладчик и достаточно удобно работать с регистрами во время отладки.
Когда я попытался сделать первый проект меня ждало разочарование — CMSIS! Кому как, но для меня это было (и остается) ужасом: много буков, длинные и для меня не понятные структуры. Вникать во все это было не интересно. Попытался скомпилировать пару примеров и понял — это не наш метод.
Неужели нет других вариантов? Есть. Тот, встроенный в IAR: iostm32f10xx4.h и подобные инклудники. Вполне не плохо:
RCC_APB2ENR_bit.ADC1EN = 1; // включить тактирование ADC
Оставалось это запихнуть в классы и пользоваться. Так и сделал. Через какое-то время потребовалось сделать код для STM32f4xx. И тут снова засада — нет инклудиков. Что делать? — писать самому. Проанализировал имеющиеся самописные библиотеки решил немного сделать по другому. Вот об этом и будет рассказ.
Начало
Про установку IAR и драйверов для отладчика рассказывать не буду, т.к. здесь ничего нового. У меня стоит IAR 8 с ограниченем кода в 32кБ. Для работы выбран контроллер STM32F103, установленный на плате plue pill.
Запускаем IAR, создаем проект c++, выбираем нужный контроллер
Следующий шаг — изучение документации. Нас будет интересовать Reference manual RM0008. Там главное внимательно читать.
Вообще, когда я обучал своих работников программированию контроллеров, я давал задание — включить светодиод (подключенный к ножке контроллера), использую дебагер, редактирую регистры и читая документацию.
Модуль RCC. Такирование
Про этот модуль обычно забывают. Вспоминают только тогда, когда не получается мигнуть светодиодом.
Запомните! Что бы включить какую-либо периферию, на нее надо подать тактовые импульсы! Без этого никак.
Порты ввода-вывода сидят на шине APB2. Находим в документации регист для упрвления тактированием этой шины, это RCC_APB2ENR:
Чтобы включить тактирование порта C (светодиод как раз припаян к PC13), требуется записать в бит IOPCEN единичку.
Теперь найдем адрес регистра RCC_APB2ENR. Смещение у него 0x18, базовый адрес для регистров RCC 0x40021000.
Чтобы удобно было работать с битами, создадим структуру:
typedef struct
{
uint32_t AFIOEN : 1;
uint32_t : 1;
uint32_t IOPAEN : 1;
uint32_t IOPBEN : 1;
uint32_t IOPCEN : 1;
uint32_t IOPDEN : 1;
uint32_t IOPEEN : 1;
uint32_t : 2;
uint32_t ADC1EN : 1;
uint32_t ADC2EN : 1;
uint32_t TIM1EN : 1;
uint32_t SPI1EN : 1;
uint32_t : 1;
uint32_t USART1EN : 1;
uint32_t :17;
} RCC_APB2ENR_b;
Чтобы потом не мучаться, сразу перечислим все адреса регистров
enum AddrRCC
{
RCC_CR = 0x40021000,
RCC_CFGR = 0x40021004,
RCC_CIR = 0x40021008,
RCC_APB2RSTR = 0x4002100C,
RCC_APB1RSTR = 0x40021010,
RCC_AHBENR = 0x40021014,
RCC_APB2ENR = 0x40021018,
RCC_APB1ENR = 0x4002101C,
RCC_BDCR = 0x40021020,
RCC_CSR = 0x40021024
};
теперь остается написать код для включения периферии
static void EnablePort(uint8_t port_name)
{
volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
switch (port_name)
{
case 'A': apb2enr->IOPAEN = 1; break;
case 'a': apb2enr->IOPAEN = 1; break;
case 'B': apb2enr->IOPBEN = 1; break;
case 'b': apb2enr->IOPBEN = 1; break;
case 'C': apb2enr->IOPCEN = 1; break;
case 'c': apb2enr->IOPCEN = 1; break;
case 'D': apb2enr->IOPDEN = 1; break;
case 'd': apb2enr->IOPDEN = 1; break;
case 'E': apb2enr->IOPEEN = 1; break;
case 'e': apb2enr->IOPEEN = 1; break;
}
}
При работе с регистрами не забываем про volatile, иначе после оптимизации компилятором долго будем искать ошибки и ругать разработчиков компилятора.
Тоже самое делаем для включения тактирвания другой периферии.
В итоге получился такой класс (не все перечислено)
STM32F1xx_RCC.h
SPL
#pragma once
#include "stdint.h"
namespace STM32F1xx
{
class RCC
{
protected:
enum AddrRCC
{
RCC_CR = 0x40021000,
RCC_CFGR = 0x40021004,
RCC_CIR = 0x40021008,
RCC_APB2RSTR = 0x4002100C,
RCC_APB1RSTR = 0x40021010,
RCC_AHBENR = 0x40021014,
RCC_APB2ENR = 0x40021018,
RCC_APB1ENR = 0x4002101C,
RCC_BDCR = 0x40021020,
RCC_CSR = 0x40021024
};
typedef struct {
uint32_t HSION : 1;
uint32_t HSIRDY : 1;
uint32_t : 1;
uint32_t HSI_TRIM : 5;
uint32_t HSI_CAL : 8;
uint32_t HSEON : 1;
uint32_t HSERDY : 1;
uint32_t HSEBYP : 1;
uint32_t CSSON : 1;
uint32_t : 4;
uint32_t PLLON : 1;
uint32_t PLLRDY : 1;
uint32_t : 6;
} RCC_CR_b;
typedef struct {
uint32_t SW : 2;
uint32_t SWS : 2;
uint32_t HPRE : 4;
uint32_t PPRE1 : 3;
uint32_t PPRE2 : 3;
uint32_t ADC_PRE : 2;
uint32_t PLLSRC : 1;
uint32_t PLLXTPRE : 1;
uint32_t PLLMUL : 4;
uint32_t USBPRE : 1;
uint32_t : 1;
uint32_t MCO : 3;
uint32_t : 5;
} RCC_CFGR_b;
typedef struct
{
uint32_t TIM2EN : 1;
uint32_t TIM3EN : 1;
uint32_t TIM4EN : 1;
uint32_t : 8;
uint32_t WWDGEN : 1;
uint32_t : 2;
uint32_t SPI2EN : 1;
uint32_t : 2;
uint32_t USART2EN : 1;
uint32_t USART3EN : 1;
uint32_t : 2;
uint32_t I2C1EN : 1;
uint32_t I2C2EN : 1;
uint32_t USBEN : 1;
uint32_t : 1;
uint32_t CANEN : 1;
uint32_t : 1;
uint32_t BKPEN : 1;
uint32_t PWREN : 1;
uint32_t : 3;
} RCC_APB1ENR_b;
typedef struct
{
uint32_t AFIOEN : 1;
uint32_t : 1;
uint32_t IOPAEN : 1;
uint32_t IOPBEN : 1;
uint32_t IOPCEN : 1;
uint32_t IOPDEN : 1;
uint32_t IOPEEN : 1;
uint32_t : 2;
uint32_t ADC1EN : 1;
uint32_t ADC2EN : 1;
uint32_t TIM1EN : 1;
uint32_t SPI1EN : 1;
uint32_t : 1;
uint32_t USART1EN : 1;
uint32_t :17;
} RCC_APB2ENR_b;
typedef struct {
uint32_t DMAEN : 1;
uint32_t : 1;
uint32_t SRAMEN : 1;
uint32_t : 1;
uint32_t FLITFEN : 1;
uint32_t : 1;
uint32_t CRCEN : 1;
uint32_t :25;
} RCC_AHBENR_r;
public:
static void EnablePort(uint8_t port_name)
{
volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
switch (port_name)
{
case 'A': apb2enr->IOPAEN = 1; break;
case 'a': apb2enr->IOPAEN = 1; break;
case 'B': apb2enr->IOPBEN = 1; break;
case 'b': apb2enr->IOPBEN = 1; break;
case 'C': apb2enr->IOPCEN = 1; break;
case 'c': apb2enr->IOPCEN = 1; break;
case 'D': apb2enr->IOPDEN = 1; break;
case 'd': apb2enr->IOPDEN = 1; break;
case 'E': apb2enr->IOPEEN = 1; break;
case 'e': apb2enr->IOPEEN = 1; break;
}
}
static void DisablePort(char port_name)
{
volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
switch (port_name)
{
case 'A': apb2enr->IOPAEN = 0; break;
case 'a': apb2enr->IOPAEN = 0; break;
case 'B': apb2enr->IOPBEN = 0; break;
case 'b': apb2enr->IOPBEN = 0; break;
case 'C': apb2enr->IOPCEN = 0; break;
case 'c': apb2enr->IOPCEN = 0; break;
case 'D': apb2enr->IOPDEN = 0; break;
case 'd': apb2enr->IOPDEN = 0; break;
case 'E': apb2enr->IOPEEN = 0; break;
case 'e': apb2enr->IOPEEN = 0; break;
}
}
static void EnableAFIO()
{
volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
apb2enr->AFIOEN = 1;
}
static void DisableAFIO()
{
volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
apb2enr->AFIOEN = 0;
}
static void EnableI2C(int PortNumber)
{
switch (PortNumber)
{
case 1:
{
volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);
apb1enr->I2C1EN = 1;
break;
}
case 2:
{
volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);
apb1enr->I2C2EN = 1;
break;
}
}
}
static void EnableUART(int PortNumber)
{
switch (PortNumber)
{
case 1:
{
volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
apb2enr->USART1EN = 1;
break;
}
case 2:
{
volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);
apb1enr->USART2EN = 1;
break;
}
case 3:
{
volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);
apb1enr->USART3EN = 1;
break;
}
}
}
static void DisableUART(int PortNumber)
{
switch (PortNumber)
{
case 1:
{
volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
apb2enr->USART1EN = 0;
break;
}
case 2:
{
volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);
apb1enr->USART2EN = 0;
break;
}
case 3:
{
volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);
apb1enr->USART3EN = 0;
break;
}
}
}
static void EnableSPI(int PortNumber)
{
switch (PortNumber)
{
case 1:
{
volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
apb2enr->SPI1EN = 1;
break;
}
case 2:
{
volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);
apb1enr->SPI2EN = 1;
break;
}
}
}
static void DisableSPI(int PortNumber)
{
switch (PortNumber)
{
case 1:
{
volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
apb2enr->SPI1EN = 0;
break;
}
case 2:
{
volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);
apb1enr->SPI2EN = 0;
break;
}
}
}
static void EnableDMA()
{
volatile RCC_AHBENR_r* ahbenr = reinterpret_cast<RCC_AHBENR_r*>(RCC_AHBENR);
ahbenr->DMAEN = 1;
}
static void DisableDMA()
{
volatile RCC_AHBENR_r* ahbenr = reinterpret_cast<RCC_AHBENR_r*>(RCC_AHBENR);
ahbenr->DMAEN = 0;
}
};
}
Теперь можно в main.cpp присоединить файл и пользоваться:
#include "STM32F1xx_RCC.h"
using namespace STM32F1xx;
int main()
{
RCC::EnablePort('c');
return 0;
}
Теперь можно и с портами поработать. GPIO
Открываем в документации раздел General-purpose and alternate-function I/Os. Находим Port bit configuration table:
Битами CNF[1:0] задается режим работы порта (аналоговый вход, цифровой вход, выход), биты MODE[1:0] отвечат за скорость работы порта в режиме выход.
Взглянем на регистры GPIOx_CRL и GPIOx_CRH (x=A, B, C,...)
видно, что биты идут последовательно:
CNF[1:0], MODE[1:0]
тогда создадим константы с режимами работы портов
enum mode_e
{
ANALOGINPUT = 0,
INPUT = 4,
INPUTPULLED = 8,
OUTPUT_10MHZ = 1,
OUTPUT_OD_10MHZ = 5,
ALT_OUTPUT_10MHZ = 9,
ALT_OUTPUT_OD_10MHZ = 13,
OUTPUT_50MHZ = 3,
OUTPUT_OD_50MHZ = 7,
ALT_OUTPUT_50MHZ = 11,
ALT_OUTPUT_OD_50MHZ = 15,
OUTPUT_2MHZ = 2,
OUTPUT_OD_2MHZ = 6,
ALT_OUTPUT_2MHZ = 10,
ALT_OUTPUT_OD_2MHZ = 14,
OUTPUT = 3,
OUTPUT_OD = 7,
ALT_OUTPUT = 11,
ALT_OUTPUT_OD = 15
};
тогда метод для конфигурации будет выглядеть так:
// pin_number - номер порта
void Mode(mode_e mode)
{
uint32_t* addr;
if(pin_number > 7)
addr = reinterpret_cast<uint32_t*>(GPIOA_CRH);
else
addr = reinterpret_cast<uint32_t*>(GPIOA_CRL);
int bit_offset;
if(pin_number > 7)
bit_offset = (pin_number - 8) * 4;
else
bit_offset = pin_number * 4;
uint32_t mask = ~(15 << bit_offset);
*addr &= mask;
*addr |= ((int)mode) << bit_offset;
}
теперь можно сделать более удобные методы для выбора режима:
void ModeInput() { Mode(INPUT); }
void ModeAnalogInput() { Mode(ANALOGINPUT); }
void ModeInputPulled() { Mode(INPUTPULLED); }
void ModeOutput() { Mode(OUTPUT); }
void ModeOutputOpenDrain() { Mode(OUTPUT_OD); }
void ModeAlternate() { Mode(ALT_OUTPUT); }
void ModeAlternateOpenDrain() { Mode(ALT_OUTPUT_OD); }
В документации находим адреса управляющих регистров для портов и перечислим
enum AddrGPIO
{
PortA = 0x40010800,
GPIOA_CRL = 0x40010800,
GPIOA_CRH = 0x40010804,
GPIOA_IDR = 0x40010808,
GPIOA_ODR = 0x4001080C,
GPIOA_BSRR = 0x40010810,
GPIOA_BRR = 0x40010814,
GPIOA_LCKR = 0x40010818,
PortB = 0x40010C00,
PortC = 0x40011000,
PortD = 0x40011400,
PortE = 0x40011800,
PortF = 0x40011C00,
PortG = 0x40012000
};
Долго думал использовать базовый адрес и смещения или абсолютные адреса. В итоге остановился на последнем. Это добавляет некоторые издержки, но в процессе отладки удобней находить в памяти.
Модернизируем метод
if(pin_number > 7)
addr = reinterpret_cast<uint32_t*>(GPIOA_CRH - PortA + PortAddr);
else
addr = reinterpret_cast<uint32_t*>(GPIOA_CRL - PortA + PortAddr);
Возможно, у кого-то будет глаз дергаться, но красивее пока не придумал.
Чтобы перевести ножку в нужное логическое состояние, достаточно записать соответствующий бит в регистре ODRx. Например, так:
void Set(bool st)
{
uint32_t* addr;
addr = reinterpret_cast<uint32_t*>(GPIOA_ODR - PortA + PortAddr);
if(st)
*addr |= 1 << pin_number;
else
{
int mask = ~(1 << pin_number);
*addr &= mask;
}
}
Также для управления состоянием можно воспользоваться регистрами GPIOx_BSRR.
По аналогии делаем методы для считывания состояния порта, методы для конфигурации и инициализации (не забываем включить тактирование). В итоге получился такой класс для работы с портами
STM32F1xx_Pin.h
SPL
#pragma once
#include <stdint.h>
#include "STM32F1xx_RCC.h"
namespace STM32F1xx
{
class Pin
{
public:
enum mode_e
{
ANALOGINPUT = 0,
INPUT = 4,
INPUTPULLED = 8,
OUTPUT_10MHZ = 1,
OUTPUT_OD_10MHZ = 5,
ALT_OUTPUT_10MHZ = 9,
ALT_OUTPUT_OD_10MHZ = 13,
OUTPUT_50MHZ = 3,
OUTPUT_OD_50MHZ = 7,
ALT_OUTPUT_50MHZ = 11,
ALT_OUTPUT_OD_50MHZ = 15,
OUTPUT_2MHZ = 2,
OUTPUT_OD_2MHZ = 6,
ALT_OUTPUT_2MHZ = 10,
ALT_OUTPUT_OD_2MHZ = 14,
OUTPUT = 3,
OUTPUT_OD = 7,
ALT_OUTPUT = 11,
ALT_OUTPUT_OD = 15
};
private:
enum AddrGPIO
{
PortA = 0x40010800,
GPIOA_CRL = 0x40010800,
GPIOA_CRH = 0x40010804,
GPIOA_IDR = 0x40010808,
GPIOA_ODR = 0x4001080C,
GPIOA_BSRR = 0x40010810,
GPIOA_BRR = 0x40010814,
GPIOA_LCKR = 0x40010818,
PortB = 0x40010C00,
PortC = 0x40011000,
PortD = 0x40011400,
PortE = 0x40011800,
PortF = 0x40011C00,
PortG = 0x40012000
};
private:
int pin_number;
int PortAddr;
public:
Pin() { }
Pin(char port_name, int pin_number) { Init(port_name, pin_number); }
~Pin()
{
Off();
ModeAnalogInput();
}
public:
void Init(char port_name, int pin_number)
{
this->pin_number = pin_number;
RCC::EnablePort(port_name);
switch (port_name)
{
case 'A': PortAddr = PortA; break;
case 'a': PortAddr = PortA; break;
case 'B': PortAddr = PortB; break;
case 'b': PortAddr = PortB; break;
case 'C': PortAddr = PortC; break;
case 'c': PortAddr = PortC; break;
case 'D': PortAddr = PortD; break;
case 'd': PortAddr = PortD; break;
case 'E': PortAddr = PortE; break;
case 'e': PortAddr = PortE; break;
}
}
void ModeInput() { Mode(INPUT); }
void ModeAnalogInput() { Mode(ANALOGINPUT); }
void ModeInputPulled() { Mode(INPUTPULLED); }
void ModeOutput() { Mode(OUTPUT); }
void ModeOutputOpenDrain() { Mode(OUTPUT_OD); }
void ModeAlternate() { Mode(ALT_OUTPUT); }
void ModeAlternateOpenDrain() { Mode(ALT_OUTPUT_OD); }
void NoPullUpDown()
{
uint32_t* addr;
if(pin_number > 7)
addr = reinterpret_cast<uint32_t*>(GPIOA_CRH - PortA + PortAddr);
else
addr = reinterpret_cast<uint32_t*>(GPIOA_CRL - PortA + PortAddr);
int bit_offset;
if(pin_number > 7)
bit_offset = (pin_number - 8) * 4;
else
bit_offset = pin_number * 4;
int mask = ~((1 << 3) << bit_offset);
*addr &= mask;
}
void Mode(mode_e mode)
{
uint32_t* addr;
if(pin_number > 7)
addr = reinterpret_cast<uint32_t*>(GPIOA_CRH - PortA + PortAddr);
else
addr = reinterpret_cast<uint32_t*>(GPIOA_CRL - PortA + PortAddr);
int bit_offset;
if(pin_number > 7)
bit_offset = (pin_number - 8) * 4;
else
bit_offset = pin_number * 4;
uint32_t mask = ~(15 << bit_offset);
*addr &= mask;
*addr |= ((int)mode) << bit_offset;
}
void Set(bool st)
{
uint32_t* addr;
addr = reinterpret_cast<uint32_t*>(GPIOA_ODR - PortA + PortAddr);
if(st)
*addr |= 1 << pin_number;
else
{
int mask = ~(1 << pin_number);
*addr &= mask;
}
}
void On()
{
uint32_t* addr;
addr = reinterpret_cast<uint32_t*>(GPIOA_ODR - PortA + PortAddr);
int bit_offset = pin_number;
*addr |= 1 << bit_offset;
}
void Off()
{
uint32_t* addr;
addr = reinterpret_cast<uint32_t*>(GPIOA_ODR - PortA + PortAddr);
int bit_offset = pin_number;
int mask = ~(1 << bit_offset);
*addr &= mask;
}
bool Get()
{
uint32_t* addr = reinterpret_cast<uint32_t*>(GPIOA_IDR - PortA + PortAddr);
int bit_offset = pin_number;
int mask = (1 << bit_offset);
bool ret_val = (*addr & mask);
return ret_val;
}
};
};
Ну что, опробуем:
#include "STM32F1xx_Pin.h"
using namespace STM32F1xx;
Pin led('c', 13);
int main()
{
led.ModeOutput();
led.On();
led.Off();
return 0;
}
Проходим дебагером и убеждаемся, что светодиод сначала загорается (после led.ModeOutput();), потом гаснет (led.On();) и снова загорается (led.Off();). Это связано с тем, что светодиод подклчен к ножке через линию питания. Поэтому, когда на выводе низкий уровень, светодиод загорается.
Не большие итоги
В данной статье я попытался (надеюсь, получилось) показать как можно немного упростить себе жизнь, сделать код более читаемым. Или наоборот — как нельзя делать. Каждый решит сам.
Можно было просто написать враперы для CMSIS, но это не интересно.
Спасибо за уделенное время. Если интересно продолжение — дайте знать.
===========
Источник:
habr.com
===========
Похожие новости:
- [C++, Анализ и проектирование систем, ООП, Программирование, Программирование микроконтроллеров] Micro Property — минималистичный сериализатор двоичных данных для embedded систем
- [C++] C++ enum <-> string? Легко
- [C++, Программирование] Антипаттерн “константа размера массива” (перевод)
- [Разработка веб-сайтов, Python, Программирование, C++] Разработка python module, чтобы продакшн радовал
- [Занимательные задачки, Программирование, C++, Алгоритмы] Обобщение классической задачи на множества с собеседований
- [Информационная безопасность, DevOps] DevSecOps: организация фаззинга исходного кода
- [Qt, Карьера в IT-индустрии, История IT, Биографии гиков] Как Qt сделал студента [человеком]
- [Open source, Программирование микроконтроллеров, Системное программирование] Разбираемся в особенностях графической подсистемы микроконтроллеров
- [C++, Программирование] ИСО одобрила С++ 20, стандарт будет опубликован к концу года
- [C++, Визуализация данных, Программирование, Учебный процесс в IT] Красиво? Очень! Как мы написали приложение для визуализации аттракторов
Теги для поиска: #_programmirovanie_mikrokontrollerov (Программирование микроконтроллеров), #_stm32, #_iar, #_c++, #_programmirovanie_mikrokontrollerov (
Программирование микроконтроллеров
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 23:28
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Всех приветствую! Это моя первая статья на хабре, поэтому прошу не кидаться тяжелыми предметами. Заранее спасибо. Начнем с предыстории. Когда-то мне пришлось перейти на микроконтроллеры ARM фирмы ST. Это было связано с тем, что PIC и AVR уже не хватало и хотелось новых приключений. Из доступного в хлебобулочных магазинах и большого количества статей о «быстром старте» выбор пал именно на STM32F100. Я привык работать в IAR. Да, есть другие IDE, но мне хватает возможности IAR: относительно удобный редактор, не плохой отладчик и достаточно удобно работать с регистрами во время отладки. Когда я попытался сделать первый проект меня ждало разочарование — CMSIS! Кому как, но для меня это было (и остается) ужасом: много буков, длинные и для меня не понятные структуры. Вникать во все это было не интересно. Попытался скомпилировать пару примеров и понял — это не наш метод. Неужели нет других вариантов? Есть. Тот, встроенный в IAR: iostm32f10xx4.h и подобные инклудники. Вполне не плохо: RCC_APB2ENR_bit.ADC1EN = 1; // включить тактирование ADC
Оставалось это запихнуть в классы и пользоваться. Так и сделал. Через какое-то время потребовалось сделать код для STM32f4xx. И тут снова засада — нет инклудиков. Что делать? — писать самому. Проанализировал имеющиеся самописные библиотеки решил немного сделать по другому. Вот об этом и будет рассказ. Начало Про установку IAR и драйверов для отладчика рассказывать не буду, т.к. здесь ничего нового. У меня стоит IAR 8 с ограниченем кода в 32кБ. Для работы выбран контроллер STM32F103, установленный на плате plue pill. Запускаем IAR, создаем проект c++, выбираем нужный контроллер Следующий шаг — изучение документации. Нас будет интересовать Reference manual RM0008. Там главное внимательно читать. Вообще, когда я обучал своих работников программированию контроллеров, я давал задание — включить светодиод (подключенный к ножке контроллера), использую дебагер, редактирую регистры и читая документацию. Модуль RCC. Такирование Про этот модуль обычно забывают. Вспоминают только тогда, когда не получается мигнуть светодиодом. Запомните! Что бы включить какую-либо периферию, на нее надо подать тактовые импульсы! Без этого никак. Порты ввода-вывода сидят на шине APB2. Находим в документации регист для упрвления тактированием этой шины, это RCC_APB2ENR: Чтобы включить тактирование порта C (светодиод как раз припаян к PC13), требуется записать в бит IOPCEN единичку. Теперь найдем адрес регистра RCC_APB2ENR. Смещение у него 0x18, базовый адрес для регистров RCC 0x40021000. Чтобы удобно было работать с битами, создадим структуру: typedef struct
{ uint32_t AFIOEN : 1; uint32_t : 1; uint32_t IOPAEN : 1; uint32_t IOPBEN : 1; uint32_t IOPCEN : 1; uint32_t IOPDEN : 1; uint32_t IOPEEN : 1; uint32_t : 2; uint32_t ADC1EN : 1; uint32_t ADC2EN : 1; uint32_t TIM1EN : 1; uint32_t SPI1EN : 1; uint32_t : 1; uint32_t USART1EN : 1; uint32_t :17; } RCC_APB2ENR_b; Чтобы потом не мучаться, сразу перечислим все адреса регистров enum AddrRCC
{ RCC_CR = 0x40021000, RCC_CFGR = 0x40021004, RCC_CIR = 0x40021008, RCC_APB2RSTR = 0x4002100C, RCC_APB1RSTR = 0x40021010, RCC_AHBENR = 0x40021014, RCC_APB2ENR = 0x40021018, RCC_APB1ENR = 0x4002101C, RCC_BDCR = 0x40021020, RCC_CSR = 0x40021024 }; теперь остается написать код для включения периферии static void EnablePort(uint8_t port_name)
{ volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR); switch (port_name) { case 'A': apb2enr->IOPAEN = 1; break; case 'a': apb2enr->IOPAEN = 1; break; case 'B': apb2enr->IOPBEN = 1; break; case 'b': apb2enr->IOPBEN = 1; break; case 'C': apb2enr->IOPCEN = 1; break; case 'c': apb2enr->IOPCEN = 1; break; case 'D': apb2enr->IOPDEN = 1; break; case 'd': apb2enr->IOPDEN = 1; break; case 'E': apb2enr->IOPEEN = 1; break; case 'e': apb2enr->IOPEEN = 1; break; } } При работе с регистрами не забываем про volatile, иначе после оптимизации компилятором долго будем искать ошибки и ругать разработчиков компилятора. Тоже самое делаем для включения тактирвания другой периферии. В итоге получился такой класс (не все перечислено) STM32F1xx_RCC.hSPL#pragma once
#include "stdint.h" namespace STM32F1xx { class RCC { protected: enum AddrRCC { RCC_CR = 0x40021000, RCC_CFGR = 0x40021004, RCC_CIR = 0x40021008, RCC_APB2RSTR = 0x4002100C, RCC_APB1RSTR = 0x40021010, RCC_AHBENR = 0x40021014, RCC_APB2ENR = 0x40021018, RCC_APB1ENR = 0x4002101C, RCC_BDCR = 0x40021020, RCC_CSR = 0x40021024 }; typedef struct { uint32_t HSION : 1; uint32_t HSIRDY : 1; uint32_t : 1; uint32_t HSI_TRIM : 5; uint32_t HSI_CAL : 8; uint32_t HSEON : 1; uint32_t HSERDY : 1; uint32_t HSEBYP : 1; uint32_t CSSON : 1; uint32_t : 4; uint32_t PLLON : 1; uint32_t PLLRDY : 1; uint32_t : 6; } RCC_CR_b; typedef struct { uint32_t SW : 2; uint32_t SWS : 2; uint32_t HPRE : 4; uint32_t PPRE1 : 3; uint32_t PPRE2 : 3; uint32_t ADC_PRE : 2; uint32_t PLLSRC : 1; uint32_t PLLXTPRE : 1; uint32_t PLLMUL : 4; uint32_t USBPRE : 1; uint32_t : 1; uint32_t MCO : 3; uint32_t : 5; } RCC_CFGR_b; typedef struct { uint32_t TIM2EN : 1; uint32_t TIM3EN : 1; uint32_t TIM4EN : 1; uint32_t : 8; uint32_t WWDGEN : 1; uint32_t : 2; uint32_t SPI2EN : 1; uint32_t : 2; uint32_t USART2EN : 1; uint32_t USART3EN : 1; uint32_t : 2; uint32_t I2C1EN : 1; uint32_t I2C2EN : 1; uint32_t USBEN : 1; uint32_t : 1; uint32_t CANEN : 1; uint32_t : 1; uint32_t BKPEN : 1; uint32_t PWREN : 1; uint32_t : 3; } RCC_APB1ENR_b; typedef struct { uint32_t AFIOEN : 1; uint32_t : 1; uint32_t IOPAEN : 1; uint32_t IOPBEN : 1; uint32_t IOPCEN : 1; uint32_t IOPDEN : 1; uint32_t IOPEEN : 1; uint32_t : 2; uint32_t ADC1EN : 1; uint32_t ADC2EN : 1; uint32_t TIM1EN : 1; uint32_t SPI1EN : 1; uint32_t : 1; uint32_t USART1EN : 1; uint32_t :17; } RCC_APB2ENR_b; typedef struct { uint32_t DMAEN : 1; uint32_t : 1; uint32_t SRAMEN : 1; uint32_t : 1; uint32_t FLITFEN : 1; uint32_t : 1; uint32_t CRCEN : 1; uint32_t :25; } RCC_AHBENR_r; public: static void EnablePort(uint8_t port_name) { volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR); switch (port_name) { case 'A': apb2enr->IOPAEN = 1; break; case 'a': apb2enr->IOPAEN = 1; break; case 'B': apb2enr->IOPBEN = 1; break; case 'b': apb2enr->IOPBEN = 1; break; case 'C': apb2enr->IOPCEN = 1; break; case 'c': apb2enr->IOPCEN = 1; break; case 'D': apb2enr->IOPDEN = 1; break; case 'd': apb2enr->IOPDEN = 1; break; case 'E': apb2enr->IOPEEN = 1; break; case 'e': apb2enr->IOPEEN = 1; break; } } static void DisablePort(char port_name) { volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR); switch (port_name) { case 'A': apb2enr->IOPAEN = 0; break; case 'a': apb2enr->IOPAEN = 0; break; case 'B': apb2enr->IOPBEN = 0; break; case 'b': apb2enr->IOPBEN = 0; break; case 'C': apb2enr->IOPCEN = 0; break; case 'c': apb2enr->IOPCEN = 0; break; case 'D': apb2enr->IOPDEN = 0; break; case 'd': apb2enr->IOPDEN = 0; break; case 'E': apb2enr->IOPEEN = 0; break; case 'e': apb2enr->IOPEEN = 0; break; } } static void EnableAFIO() { volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR); apb2enr->AFIOEN = 1; } static void DisableAFIO() { volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR); apb2enr->AFIOEN = 0; } static void EnableI2C(int PortNumber) { switch (PortNumber) { case 1: { volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR); apb1enr->I2C1EN = 1; break; } case 2: { volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR); apb1enr->I2C2EN = 1; break; } } } static void EnableUART(int PortNumber) { switch (PortNumber) { case 1: { volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR); apb2enr->USART1EN = 1; break; } case 2: { volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR); apb1enr->USART2EN = 1; break; } case 3: { volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR); apb1enr->USART3EN = 1; break; } } } static void DisableUART(int PortNumber) { switch (PortNumber) { case 1: { volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR); apb2enr->USART1EN = 0; break; } case 2: { volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR); apb1enr->USART2EN = 0; break; } case 3: { volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR); apb1enr->USART3EN = 0; break; } } } static void EnableSPI(int PortNumber) { switch (PortNumber) { case 1: { volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR); apb2enr->SPI1EN = 1; break; } case 2: { volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR); apb1enr->SPI2EN = 1; break; } } } static void DisableSPI(int PortNumber) { switch (PortNumber) { case 1: { volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR); apb2enr->SPI1EN = 0; break; } case 2: { volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR); apb1enr->SPI2EN = 0; break; } } } static void EnableDMA() { volatile RCC_AHBENR_r* ahbenr = reinterpret_cast<RCC_AHBENR_r*>(RCC_AHBENR); ahbenr->DMAEN = 1; } static void DisableDMA() { volatile RCC_AHBENR_r* ahbenr = reinterpret_cast<RCC_AHBENR_r*>(RCC_AHBENR); ahbenr->DMAEN = 0; } }; } Теперь можно в main.cpp присоединить файл и пользоваться: #include "STM32F1xx_RCC.h"
using namespace STM32F1xx; int main() { RCC::EnablePort('c'); return 0; } Теперь можно и с портами поработать. GPIO Открываем в документации раздел General-purpose and alternate-function I/Os. Находим Port bit configuration table: Битами CNF[1:0] задается режим работы порта (аналоговый вход, цифровой вход, выход), биты MODE[1:0] отвечат за скорость работы порта в режиме выход. Взглянем на регистры GPIOx_CRL и GPIOx_CRH (x=A, B, C,...) видно, что биты идут последовательно: CNF[1:0], MODE[1:0] тогда создадим константы с режимами работы портов enum mode_e
{ ANALOGINPUT = 0, INPUT = 4, INPUTPULLED = 8, OUTPUT_10MHZ = 1, OUTPUT_OD_10MHZ = 5, ALT_OUTPUT_10MHZ = 9, ALT_OUTPUT_OD_10MHZ = 13, OUTPUT_50MHZ = 3, OUTPUT_OD_50MHZ = 7, ALT_OUTPUT_50MHZ = 11, ALT_OUTPUT_OD_50MHZ = 15, OUTPUT_2MHZ = 2, OUTPUT_OD_2MHZ = 6, ALT_OUTPUT_2MHZ = 10, ALT_OUTPUT_OD_2MHZ = 14, OUTPUT = 3, OUTPUT_OD = 7, ALT_OUTPUT = 11, ALT_OUTPUT_OD = 15 }; тогда метод для конфигурации будет выглядеть так: // pin_number - номер порта
void Mode(mode_e mode) { uint32_t* addr; if(pin_number > 7) addr = reinterpret_cast<uint32_t*>(GPIOA_CRH); else addr = reinterpret_cast<uint32_t*>(GPIOA_CRL); int bit_offset; if(pin_number > 7) bit_offset = (pin_number - 8) * 4; else bit_offset = pin_number * 4; uint32_t mask = ~(15 << bit_offset); *addr &= mask; *addr |= ((int)mode) << bit_offset; } теперь можно сделать более удобные методы для выбора режима: void ModeInput() { Mode(INPUT); }
void ModeAnalogInput() { Mode(ANALOGINPUT); } void ModeInputPulled() { Mode(INPUTPULLED); } void ModeOutput() { Mode(OUTPUT); } void ModeOutputOpenDrain() { Mode(OUTPUT_OD); } void ModeAlternate() { Mode(ALT_OUTPUT); } void ModeAlternateOpenDrain() { Mode(ALT_OUTPUT_OD); } В документации находим адреса управляющих регистров для портов и перечислим enum AddrGPIO
{ PortA = 0x40010800, GPIOA_CRL = 0x40010800, GPIOA_CRH = 0x40010804, GPIOA_IDR = 0x40010808, GPIOA_ODR = 0x4001080C, GPIOA_BSRR = 0x40010810, GPIOA_BRR = 0x40010814, GPIOA_LCKR = 0x40010818, PortB = 0x40010C00, PortC = 0x40011000, PortD = 0x40011400, PortE = 0x40011800, PortF = 0x40011C00, PortG = 0x40012000 }; Долго думал использовать базовый адрес и смещения или абсолютные адреса. В итоге остановился на последнем. Это добавляет некоторые издержки, но в процессе отладки удобней находить в памяти. Модернизируем метод if(pin_number > 7)
addr = reinterpret_cast<uint32_t*>(GPIOA_CRH - PortA + PortAddr); else addr = reinterpret_cast<uint32_t*>(GPIOA_CRL - PortA + PortAddr); Возможно, у кого-то будет глаз дергаться, но красивее пока не придумал. Чтобы перевести ножку в нужное логическое состояние, достаточно записать соответствующий бит в регистре ODRx. Например, так: void Set(bool st)
{ uint32_t* addr; addr = reinterpret_cast<uint32_t*>(GPIOA_ODR - PortA + PortAddr); if(st) *addr |= 1 << pin_number; else { int mask = ~(1 << pin_number); *addr &= mask; } } Также для управления состоянием можно воспользоваться регистрами GPIOx_BSRR. По аналогии делаем методы для считывания состояния порта, методы для конфигурации и инициализации (не забываем включить тактирование). В итоге получился такой класс для работы с портами STM32F1xx_Pin.hSPL#pragma once
#include <stdint.h> #include "STM32F1xx_RCC.h" namespace STM32F1xx { class Pin { public: enum mode_e { ANALOGINPUT = 0, INPUT = 4, INPUTPULLED = 8, OUTPUT_10MHZ = 1, OUTPUT_OD_10MHZ = 5, ALT_OUTPUT_10MHZ = 9, ALT_OUTPUT_OD_10MHZ = 13, OUTPUT_50MHZ = 3, OUTPUT_OD_50MHZ = 7, ALT_OUTPUT_50MHZ = 11, ALT_OUTPUT_OD_50MHZ = 15, OUTPUT_2MHZ = 2, OUTPUT_OD_2MHZ = 6, ALT_OUTPUT_2MHZ = 10, ALT_OUTPUT_OD_2MHZ = 14, OUTPUT = 3, OUTPUT_OD = 7, ALT_OUTPUT = 11, ALT_OUTPUT_OD = 15 }; private: enum AddrGPIO { PortA = 0x40010800, GPIOA_CRL = 0x40010800, GPIOA_CRH = 0x40010804, GPIOA_IDR = 0x40010808, GPIOA_ODR = 0x4001080C, GPIOA_BSRR = 0x40010810, GPIOA_BRR = 0x40010814, GPIOA_LCKR = 0x40010818, PortB = 0x40010C00, PortC = 0x40011000, PortD = 0x40011400, PortE = 0x40011800, PortF = 0x40011C00, PortG = 0x40012000 }; private: int pin_number; int PortAddr; public: Pin() { } Pin(char port_name, int pin_number) { Init(port_name, pin_number); } ~Pin() { Off(); ModeAnalogInput(); } public: void Init(char port_name, int pin_number) { this->pin_number = pin_number; RCC::EnablePort(port_name); switch (port_name) { case 'A': PortAddr = PortA; break; case 'a': PortAddr = PortA; break; case 'B': PortAddr = PortB; break; case 'b': PortAddr = PortB; break; case 'C': PortAddr = PortC; break; case 'c': PortAddr = PortC; break; case 'D': PortAddr = PortD; break; case 'd': PortAddr = PortD; break; case 'E': PortAddr = PortE; break; case 'e': PortAddr = PortE; break; } } void ModeInput() { Mode(INPUT); } void ModeAnalogInput() { Mode(ANALOGINPUT); } void ModeInputPulled() { Mode(INPUTPULLED); } void ModeOutput() { Mode(OUTPUT); } void ModeOutputOpenDrain() { Mode(OUTPUT_OD); } void ModeAlternate() { Mode(ALT_OUTPUT); } void ModeAlternateOpenDrain() { Mode(ALT_OUTPUT_OD); } void NoPullUpDown() { uint32_t* addr; if(pin_number > 7) addr = reinterpret_cast<uint32_t*>(GPIOA_CRH - PortA + PortAddr); else addr = reinterpret_cast<uint32_t*>(GPIOA_CRL - PortA + PortAddr); int bit_offset; if(pin_number > 7) bit_offset = (pin_number - 8) * 4; else bit_offset = pin_number * 4; int mask = ~((1 << 3) << bit_offset); *addr &= mask; } void Mode(mode_e mode) { uint32_t* addr; if(pin_number > 7) addr = reinterpret_cast<uint32_t*>(GPIOA_CRH - PortA + PortAddr); else addr = reinterpret_cast<uint32_t*>(GPIOA_CRL - PortA + PortAddr); int bit_offset; if(pin_number > 7) bit_offset = (pin_number - 8) * 4; else bit_offset = pin_number * 4; uint32_t mask = ~(15 << bit_offset); *addr &= mask; *addr |= ((int)mode) << bit_offset; } void Set(bool st) { uint32_t* addr; addr = reinterpret_cast<uint32_t*>(GPIOA_ODR - PortA + PortAddr); if(st) *addr |= 1 << pin_number; else { int mask = ~(1 << pin_number); *addr &= mask; } } void On() { uint32_t* addr; addr = reinterpret_cast<uint32_t*>(GPIOA_ODR - PortA + PortAddr); int bit_offset = pin_number; *addr |= 1 << bit_offset; } void Off() { uint32_t* addr; addr = reinterpret_cast<uint32_t*>(GPIOA_ODR - PortA + PortAddr); int bit_offset = pin_number; int mask = ~(1 << bit_offset); *addr &= mask; } bool Get() { uint32_t* addr = reinterpret_cast<uint32_t*>(GPIOA_IDR - PortA + PortAddr); int bit_offset = pin_number; int mask = (1 << bit_offset); bool ret_val = (*addr & mask); return ret_val; } }; }; Ну что, опробуем: #include "STM32F1xx_Pin.h"
using namespace STM32F1xx; Pin led('c', 13); int main() { led.ModeOutput(); led.On(); led.Off(); return 0; } Проходим дебагером и убеждаемся, что светодиод сначала загорается (после led.ModeOutput();), потом гаснет (led.On();) и снова загорается (led.Off();). Это связано с тем, что светодиод подклчен к ножке через линию питания. Поэтому, когда на выводе низкий уровень, светодиод загорается. Не большие итоги В данной статье я попытался (надеюсь, получилось) показать как можно немного упростить себе жизнь, сделать код более читаемым. Или наоборот — как нельзя делать. Каждый решит сам. Можно было просто написать враперы для CMSIS, но это не интересно. Спасибо за уделенное время. Если интересно продолжение — дайте знать. =========== Источник: habr.com =========== Похожие новости:
Программирование микроконтроллеров ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 23:28
Часовой пояс: UTC + 5