Первое, с чем столкнулись начинающие радиолюбители, которые начали изучение STM32, это большое количество строк инициализации, некоторых людей даже отпугнул этот факт от дальнейшего изучения. Поэтому немного подтянем наши знания языка Си.
Скажу честно, во времена использования AVR я макросами пользовался мало, просто не было нужды. Регистров у авр куда меньше чем у стм, поэтому было достаточно вынести инициализацию в отдельный файл. При переходе на стм, сначала я делал точно также, инициализация в отдельный файл и вроде все было нормально. Но когда пошли проекты посложнее, то стало очевидно, что в каше из SPL структур тяжело разбираться, поэтому на первое место вышел вопрос читаемости кода.
В общем то даже здесь есть разные подходы к решению проблемы, самый популярный это использование макросов. Но давайте обо всем по порядку, начнем с простых макросов.
Макросы константы. Допустим определим константу BUF_SIZE:
#define BUF_SIZE 1024 |
Теперь везде, где бы мы не написали BUF_SIZE, препроцессор будет подставлять 1024. Тогда если определить:
uint16 buffer[BUF_SIZE] |
То получим:
uint16 buffer[1024] |
Важно понимать, что препроцессор работает до компиляции проекта, поэтому подстановка будет произведена еще до того как вы скомпилируете прошивку, поэтому строки
#define BUF_SIZE 1024 uint16 buffer[BUF_SIZE] |
и
uint16 buffer[1024] |
Для компилятора абсолютно эквивалентны. Количество строчек кода здесь никак не повлияет на скорость работы прошивки. Эти строки будут обработаны 1 раз вашим компом, а не микроконтроллером.
Обертывать макросы в макросы можно сколько угодно раз, можно в них вычислять выражения
#define BUF_SIZE 1024 #define USART_BUF (BUF_SIZE*2) #define PREFIX_SIZE 5 #define PROTOCOL_BUF (USART_BUF - PREFIX_SIZE) |
В результате
uint16 buffer[PROTOCOL_BUF] |
Будет преобразован в:
uint16 buffer[2043] |
Причем также, один раз, еще до того как прошивка будет залита в микроконтроллер.
Для удобочитаемости кода можно удобно обозвать названия ножек
#define LED1 PORTB.1 #define ON 1 #define OFF 0 |
Далее в основном цикле
LED1 = ON; delay_ms(100); LED1 = OFF; |
Будет преобразовано в
PORTB.1 = 1; delay_ms(100); PORTB.1 = 0; |
Самое вкусное это макросы подобные функциям. От обычных они отличаются скобками, внутри которых есть параметры. Параметры которые пользователь передаст внутри скобок, будут подставлены в тело выражения. Сами параметры разделяются запятыми. По принципу это очень похоже на функции.
Например известный макрос установки бита ножки.
#define SET_BIT(PORTx, BITx) PORTx |= (1<<BITx) |
Если в основном цикле вызвать его
SET_BIT(PORTB, 6); |
То это будет равносильно установке 6 бита порта B.
PORTB |= (1<<6) |
Да, да господа CAVRщики именно так по «правильному» включается ножка, а не
PORB.6 = 1, не забывайте это.
Погнали дальше, если какой то из параметров нужно заключить в кавычки, то перед ним нужно ставить решетку #
#define send_message(str) lcd_puts(#str); |
Теперь строка
send_message(hello); |
Будет эквивалентна
lcd_puts("hello"); |
А вот следующая фича, просто мега фича 🙂 При помощи ## можно сращивать строки. Например, если задать такой дефайн:
#define LED_PORT(x) PORT##x |
То запись
LED_PORT(B) = 0xff; |
Будет равна
PORTB = 0xff; |
Т.е. на первом шаге PORT подставился вместо LED_PORT, а параметр B, подставился вместо x,
PORT##B = 0xff; |
А двойная решетка «срастила» две разных лексемы PORT и B
PORTB = 0xff; |
При использовании макросов есть много «тонкостей». Макросы могут быть многострочные, например define x 0x09 можно записать в несколько строк, разделяя строки слешем
#define x 0\
x\
0\
9 |
Однако если в макросах есть точка с запятой
#define SETUP_PORTS() PORTB = 0xff;\
PORTC = 0xff; |
то при его использовании в теле основного цикла точка с запятой уже не понадобится
SETUP_PORTS() |
Это плохо, ибо можно ошибиться когда нужно ставить точку с запятой, а когда нет. Поэтому такие их обрамляют циклом do{}while(0). Напомню конструкция do{}while() вначале выполняется, потом проверяется условие. В нашем случае, условие ложное т.е. while(0), поэтому оно выполнится ровно один раз, что нам собственно и нужно. В этом случае точку с запятой ставить нужно.
#define SETUP_PORTS()\ do \ { \ PORTB = 0xff; \ PORTC = 0xff; \ }while(0) |
Теперь они похожи на обычные фукции
SETUP_PORTS(); |
Объединяя выше сказанное можно получить заготовку простой инициализации пина для STM32
#define gpio_config(GPIOx, PINx, MODEx, PULLx, OTypex) \
do \
{ \
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIO##GPIOx, ENABLE); \
GPIO_InitTypeDef PORT_SETUP; \
PORT_SETUP.GPIO_Mode = GPIO_Mode_##MODEx; \
PORT_SETUP.GPIO_PuPd = GPIO_PuPd_##PULLx; \
PORT_SETUP.GPIO_OType = GPIO_OType_##OTypex; \
PORT_SETUP.GPIO_Pin = GPIO_Pin_##PINx; \
PORT_SETUP.GPIO_Speed = GPIO_Speed_50MHz; \
GPIO_Init(GPIO##GPIOx, &PORT_SETUP); \
} while(0) |
Теперь всю эту ботву можно вынести в отдельный файл, а в инициализации записать так:
gpio_config(A,5,OUT,NOPULL,PP); |
Это будет значить портА, 5 нога, режим выход out, без подтяжки, режим пуш-пул.
Но фокусы на этом не заканчиваются. Эту запись можно вынести еще в отдельный файл, где будут сложены все остальные дефайны пинов.
#define LED_PIN A,5,OUT,NOPULL,PP |
Тогда в инициализации строка запишется так:
gpio_config(LED_PIN); |
Подобный подход гарантирует, что мы не будет рыться в сотнях строк кода, в поисках своей заветной инициализации. Все будет в одном месте и при понятных именах, не будет проблем с поиском нужной ножки.
Но и это еще не все 🙂 Думаю про условную компиляцию тоже многие слышали. Например зададим дефайн:
#define LCD_PORTB |
Условно говоря, если задан порт lcd то конфигурируем портB на выход, если не задан, то считаем по умолчанию что используется портD
#ifdef LCD_PORTB DDRB = 0xff; #else DDRD = 0xff; # endif |
Кроме того, существует очень интересный фокус, с глобальными переменными. Если у вас куча модулей, в каждом из которых вы должны объявить глобальный массив значений АЦП adc_buffer, то вам нужно в каждом модуле писать руками extern int adc_buffer[8] чтобы сделать ее видимой, а в главном модуле непосредственно объявлять переменную int adc_buffer[8]. Однако есть способ проще.
Создаем отдельный h файл в котором пишем следующее:
#ifdef MAIN int adc_buffer[8] #else extern int adc_buffer[8]; # endif |
В главном файле просто задаем дефайн
#define MAIN |
Теперь adc_buffer будет виден во всех файлах без лишнего гемора.
Еще пример макроса позволяющего компилировать код в зависимости от типа микроконтроллера
#if defined (STM32F10X_LD) || (STM32F10X_LD_VL) || (STM32F10X_MD) || (STM32F10X_MD_VL) || (STM32F10X_HD) || (STM32F10X_HD_VL) || (STM32F10X_XL) || (STM32F10X_CL) //тут код специфичный для stm32f1 #elif defined (STM32F40_41xxx) || (STM32F427_437xx) || (STM32F429_439xx) || (STM32F401xx) || (STM32F411xE) //тут код специфичный для stm32f4 #else //если используется какой то иной камень, выдаем ошибку #error "This mcu is not supported, or select mcu type" #endif |
В общем то как будет время «изобрету» свой велосипед с инициализацией пинов, юартов и прочее. А на текущий момент все что я хотел сказать, макросы сильно упрощают разработку и увеличивают читаемость кода. Хоть их сильно ругают и не рекомендуют пользоваться, но на мой взгляд при правильном подходе это очень хороший инструмент.
Еще из того, чем часто пользуюсь:
Если нужно использовать несколько условий типа defined, то можно делать так:
#if defined (case1) && (case2) || (case3)
action();
#endif
Всякие сообщения на этапе компиляции:
#if defined(__DEBUG)
#warning «compiling for debug mode!»
#endif
#if !defined(PAGE_SIZE)
#error «PAGE_SIZE not defined!!!»
#endif
Во время компиляции, на этапе работы препроцессора, не доступны некоторые ф-ции, к примеру sizeof(). Чтобы проверить, к примеру, не выходит ли размер какого-нибудь буффера за пределы, можно использовать такую фичу:
#define STATIC_ASSERT(e) extern char (*ct_assert(void)) [sizeof(char[1 — 2*!(e)])]
void main(){
STATIC_ASSERT(sizeof(options) < EEPROM_PAGE_SIZE);
}
Если размер options превысит EEPROM_PAGE_SIZE, компиляция прервется ошибкой.
добавил в конце, правда про ассерт не вкурил
очень интересная статья и всё доходчиво..спасибо
Если для AVR то да, а зачем для stm. Все уже придумано. Hal и никаких проблем. Я лазил по файлам после генератора, там все грамотно сделано. Единственное что нужно сделать, так это поставить галку генерить код для периферии в отдельных файлах.
Вот когда открываешь чужой проект и в нем нахерачено так, что проще с нуля переписать чем разбираться, тогда начинаешь задумываться. Поэтому для меня, первостепенно писать понятный, читаемый код. И речи не идет о том, чтобы переписывать код HAL, но об этом в ближайших статьях)
Если в проекте нахерачено так, что проще с нуля написать, то цена такому проектанту 0. В HAL как раз все понятно. А про HAL я написал прочитав это -«Первое, с чем столкнулись начинающие радиолюбители, которые начали изучение STM32, это большое количество строк инициализации».
Не зарекайтесь про проектантов Множество людей используют только CMSIS, там в общем то тоже все понятно. Халовские библиотеки для них говнокод. Этот вопрос бессмысленно обсуждать.
В этой статье я писал совсем о другом, я писал об «обертках», любую халовскую инициализацию структуры, удобнее обернуть простой фукцией/макросом, например ардуино стайл 🙂
uart_init(uart1, 9600)
Тут даже ежу понятно, что инициализируется уарт1 на скорости 9600. И это ЧИТАЕМО!!!
Сдаюсь. Согласен.
Не работает АВР-стайл на СТМ:
#define PORT##x##.##y##=1() GPIO_SetBits(GPIO##x, GPIO_Pin_##y)
и не будет, чтобы параметры подставлялись их нужно указывать в скобках. и кстати то что вы пытаетесь сделать не синктис си.