Обычно чтобы можно было легко отлаживать проект или динамически что-то настраивать хорошо иметь консоль, поэтому сразу будем настраивать USART, которых у нас присутствует аж 6 штук. Но начать надо с конфигурации самой Discovery.
Есть куча примеров низкого уровня, чем заморачиваться имеет смысл, только если вам не хватает памяти. Наиболее аппаратно независимо использовать штатные библиотеки от ST. Но ввиду того, что большинство настолько "круты", что бы использовать штатные библиотеки, что они у них не работают ("Что-то у меня не завелось сразу со штатными библиотеками, поэтому я их не использую" - типичная фраза на форумах) и проще сразу настраивать все через регистры. Но зачем изобретать велосипед, если все за нас уже написано и в целом хорошо работает? Более того навороченность периферии STM32 позволяет не терять производительность даже при использовании тяжелого кода (например, DMA).
Итак, я поставил перед собой цель использовать ТОЛЬКО штатные функции и НЕ писать самостоятельно напрямую в регистры.
Настройка Discovery.
ST предоставляет специальный помошник для конфигурации Вашего контроллера в Эксель-файле "STM32F4xx_Clock_Configuration_V1.0.0.xls". Открываем его и видим такую картину
На Discovery штатно стоит кварц 8MHz, на который нам необходимо переключиться. Делается настройкой файла "system_stm32f4xx.c", который нам и создает, указанный выше, визард. В поле HSE необходимо выставить 8 и нажать кнопочку "Run", макрос начнет что-то вычислять и через некоторое время уточнит, через что мы хотим тактироваться? Надо выбрать, то где "HSE". Еще дополнительно хорошо поставить галочку "Require 48MHz for USB OTG FS", дело в том, что некоторая периферия требует строго определенную частоту и если Вы ее хотите использовать, то необходимо об этом за ранее позаботиться. Когда макрос закончит работу нажать кнопочку "Generate" - тут же в текущей директории создастся файлик "system_stm32f4xx.c".
Я работаю в CooCox`е, который выбрал как 100% полноценно работающую IDE с Discovery и другими МК, при чем настраивать первоначально ничего не надо, но если непривычный интерфейс, то можно погуглить и найти как удобно настроить его под себя. Так вот, CooCox все необходимые библиотеки от ST копирует в Ваш проект, т.е. их можно менять как угодно и на др. проект влиять не будет, что очень удобно. На закладке "Repository" выбираем наш МК, потом выбираем галочки, что мы хотим использовать в проекте, и все библиотеки будут в него включены, но в файле "main.c" все-равно ручками надо прописать все необходимые "#include".
Далее или заменяем файлик "system_stm32f4xx.c" в нашем проекте "C:\CooCox\CoIDE\workspace\Мой_проект\cmsis_boot\" на новый или прямо в CooCox`е открываем этот файлик и простым копипастом меняем на новое содержимое.
Но и это еще не все. Чтобы все работало как надо, т.е. нигде не сбивались частоты и правильно рассчитывались скорости, необходима константа "HSE_VALUE", которую надо прописать как параметр компилятору. В этой константе должны находится частота нашего кварца и определена она должна быть для всех модулей. Идем в "Configuration" нашего проекта на закладку "Compile" и в разделе "Defined Symbols" добавляем значение "HSE_VALUE=8000000", что добавит в строку компилятора параметр "-DHSE_VALUE=8000000;".
Теперь в файле "main.c" в главной процедуре "int main(void)" можно выполнять команду "SystemInit();". А если точнее, с вызова этой функции должно все и начинаться, а только после нее инициализироваться все остальное.
Вот теперь можно запускать остальную периферию и она будет работать так как мы укажем в ее настройках. Что приятно при использовании стандартных библиотек, что не надо заморачиваться кучей расчетов и тем куда результат поместить, а просто сказать что хочу и получить это!
Например, вот так инициализируются 4 Пина для моргания 4-мя светодиодами на Discovery:
Код: Выделить всё
// Активируем татирование порта D
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE);
/* Конфигурируем пины PD12, PD13, PD14 и PD15 в режиме вывода pushpull*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13| GPIO_Pin_14| GPIO_Pin_15;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; //Выход
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; // pushpull
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; // рабочая частота выхода (посути определяет время реакции)
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; // без подтяжки
GPIO_Init(GPIOD, &GPIO_InitStructure); // Непосредсвенно применям настройки порта
Теперь можно моргать светодиодами:
Код: Выделить всё
GPIO_SetBits(GPIOD, GPIO_Pin_12); // Установить высокий уровень на пинах
GPIO_ResetBits(GPIOD, GPIO_Pin_12|GPIO_Pin_13|GPIO_Pin_14|GPIO_Pin_15); // сбросить пин (установить низкий уровень)
Активируем USART.
Очень рекомендую удобную софтину MicroXplorer от STMicroelectronics, которая вместо чтения datasheet наглядно покажет какой периферией мы располагаем и какие пины она использует и может даже помочь в их конфигурации.
Итак, если мы хотим использовать USART1, то на discovery про него лучше забыть, т.к. на PA10 не только USART1_RX, но и OTG_FS_ID, а на PA9 не только USART1_TX, но и OTG_FS_VBUS, которые на Discovery уже разведены соответствующем образом - нужны для USB! Интересно, что про USB народ помнит, а то что у STM32 можно переназначать ножки, про это забывают, т.е. USART1 можно назначить еще на ножки PB6 и PB7. Я поддался первому веянию и отказался от USART1, подключив USART3 по примеру, который открылся в CooCox`е (ну, так не получалось, а по примеру думал, что будет все хорошо).
Создаем новую функцию, для инициализации периферии:
Код: Выделить всё
void initAll()
{
// Включаем прерывания
__enable_irq();
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
/* Enable GPIO clock - Заметим, что инициализруем альтернативный порт C (штатно USART3 на порту D)*/
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, ENABLE);
/* Enable UART clock - Включаем тактирование USART3*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);
/* Connect PXx to USARTx_Tx - подключаем соответствующие (альтернативные) пины к USART3*/
GPIO_PinAFConfig(GPIOC, GPIO_PinSource10, GPIO_AF_USART3);
/* Connect PXx to USARTx_Rx*/
GPIO_PinAFConfig(GPIOC, GPIO_PinSource11, GPIO_AF_USART3);
/* Configure USART Tx as alternate function */
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; // Инициализируем и вход и выход как Alternate Function
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);
/* Configure USART Rx as alternate function */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
//GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
GPIO_Init(GPIOC, &GPIO_InitStructure);
USART_InitStructure.USART_BaudRate = 9600; // Скорость
USART_InitStructure.USART_WordLength = USART_WordLength_8b; // длина пакета 1байт/8бит
USART_InitStructure.USART_StopBits = USART_StopBits_1; // 1 стоп-бит
USART_InitStructure.USART_Parity = USART_Parity_No; // Без контроля четности
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // Разрешаем прием и передачу
/* USART configuration */
USART_Init(USART3, &USART_InitStructure);
// Разрешаем прерывания для USART3
NVIC_EnableIRQ(USART3_IRQn);
/* Enable USART - Непосредсвенно включаем*/
USART_Cmd(USART3, ENABLE);
// А для чего же должны срабатывать прерывания USART3?
USART_ITConfig(USART3, USART_IT_TC, ENABLE); // По окончанию отправки
USART_ITConfig(USART3, USART_IT_RXNE, ENABLE); // При получении
}
Теперь можно принимать и отправлять данные (ручные прием и отправка)
Код: Выделить всё
While(!USART_GetFlagStatus(USART3,USART_FLAG_TXE)!= RESET){} //Проверяем и ждем пока регистр отправки будет пуст
USART_SendData(USART3, 'a'); // Отправляем символ "a"
if( USART_GetFlagStatus(USART3,USART_FLAG_RXNE)!= RESET){ // Проверяем не пусто ли регистр приема
byte tmp=USART_ReceiveData(USART3); // Если данные есть, то их надо получить
}
Поскольку прием может начаться когда МК чем-то очень занят, то мы можем потерять полезную информацию. Чтобы этого избежать, надо использовать или прерывания или DMA. Последнее работает шустрее, но мы выше уже активировали прерывания . Заметим что какое бы событие по USART3 не произошло, обработчик всегда будет один! Чтобы узнать, что именно произошло необходимо делать доп. проверки.
Код: Выделить всё
void USART3_IRQHandler()
{
if (USART_GetITStatus(USART3, USART_IT_TC) != RESET) // Если отпавка завершена
{
// Очищаем флаг прерывания, если этого не сделать, оно будет вызываться постоянно.
USART_ClearITPendingBit(USART3, USART_IT_TC);
// ... что-то там
}
if(USART_GetITStatus(USART3, USART_IT_RXNE)!=RESET) // Если приием завершен (регистр приема не пуст)
{
// Флаг данного прерывания сбрасыывается прочтением данных
// ... что-то там
}
}
Теперь обратим внимание, что данные могут отправляться и приходить большой очередью, поэтому для приема и передачи хорошо бы завести немного буферной памяти. Поскольку данные должны помещаться и забираться независимыми процессами (будем рассматривать выполнение прерывания как отдельный процесс, т.к. мы не знаем в какой именно момент прерывание сработает), то нельзя использовать запись в одну переменную из разных мест, поэтому нам наиболее удобен будет кольцевой буфер. Кольцевой буфер удобен тем, что тот, кто кладет данные, увеличивает переменную конца списка, а тот, кто забирает данные, увеличивает переменную начала списка. Длину списка можно вычислить по разнице между началом и концом списка. Поскольку физически наш список линеен, то его надо замкнуть в кольцо искусственно, т.е. если счетчик дошел до конца списка, то необходимо перескочить в начало списка. Самое простое это делать наложением спец. маски, соответствующей размеру списка, на увеличиваемый счетчик начала или конца. Еще проще использовать размер списка, соответствующий размеру переменным начала и конца, как это сделал я (использовал буфер размером 256 байт, что соответствует максимальному значению переменной типа "uint8_t"), тогда счетчик можно тупо инкрементировать и при достижении максимального значения он автоматом перескочит на 0. Алгоритм простой: Если кладем в буфер данные, инкрементируем счетчик конца списка, если читаем данные, то инкрементируем счетчик начала списка, а читать можно, только если буфер не пуст и записывать, только если буфер не полон.
Если порыться по сети, то можно найти удобные готовые классы кольцевого буфера, но мне попадались, только избыточно навороченные или не доделанные. Решено делать свои, поэтому я не заморачивался доп проверками и сразу включил в функции чтения/записи необходимый инструментарий отправки в UART и обратно. Получилось как-то так:
Код: Выделить всё
uint8_t RxData[256]; // читаем сюда
uint8_t TxData[256]; // Передаем от сюда
volatile uint8_t RxIn = 0; // Счетчик конца - по этому адресу кладем в буфер
volatile uint8_t RxOut = 0; // Счетчик начала - по этому адресу забираем из буфера
volatile uint8_t TxIn = 0;
volatile uint8_t TxOut = 0;
// uint8_t RxEmpty=1; // Планировал использовать флаг, чтобы знать когда передача отдыхает,
//регистр отправки пуст и можно сразу отправлять данные, но так не задействовал...
void UsartTx(){ // Отправляем данные если они есть в буфере
if(TxIn != TxOut){ // проверяем количество данных в буфере
uint8_t tmp=TxData[TxOut++]; // забираем данные и инкрементируем счетчик
USART_SendData(USART3, tmp);
}
}
void UsartRx(){ // Принимаем данные если в буфере есть место. Если места нет, то данные по любому пропадут.
int tmp=RxIn-RxOut;
if(tmp<256 && tmp!=-1){
RxData[RxIn++]=USART_ReceiveData(USART3);
}
}
void UsartSend(){ // Запустить в ручную процесс передачи, если буфер не пуст и процесс уже не идет
if((USART_GetFlagStatus(USART3,USART_FLAG_TXE)!= RESET) && (TxIn != TxOut)){
UsartTx();
}
}
uint8_t UsartTestData(){ // Проверка, получили ли мы какие-нибудь данные из вне.
return RxIn!=RxOut;
}
uint8_t UsartGetData(){ // читаем данные из буфера, что же нам прислали
return RxData[RxOut++];
}
char UsartPutData(char Dt){ // Помещаем данные в буфер, чтобы их отправить наружу
int tmp=TxIn-TxOut;
if(tmp<255 && tmp!=-1){
TxData[TxIn++]=Dt;
return 1; // Если смогли поместить в буфер, то все хорошо
}
return 0; // если в буфере нет места, попробуйте позже
}
Теперь надо модифицировать обработчик прерывания
Код: Выделить всё
// Обработчик прерывания
void USART3_IRQHandler()
{
if (USART_GetITStatus(USART3, USART_IT_TC) != RESET)
{
// Очищаем флаг прерывания
USART_ClearITPendingBit(USART3, USART_IT_TC);
UsartTx();
}
if(USART_GetITStatus(USART3, USART_IT_RXNE)!=RESET)
{
UsartRx();
}
}
Тут есть один недостаток, что если буфер отправки пуст, то флаг "USART_IT_TC" все-равно уже сброшен и данное прерывание более не сработает, а если данных полно, то прерывания будут срабатывать пока буфер не очистится полностью. Т.е. если отправка была закончена, то чтобы отправку снова запустить, необходимо дернуть процедуру "UsartSend()".
Для теста я использовал такую процедуру:
Код: Выделить всё
int main(void)
{
SystemInit();
initAll();
UsartPutData('9'); // Это мы говорим, что девайс запущен
UsartPutData('8');
UsartPutData('7');
UsartPutData('6');
UsartPutData('5');
UsartPutData('4');
UsartPutData('3');
UsartPutData('2');
UsartPutData('1');
UsartPutData('0');
// отправка пойдет сразу, т.к. USART, только проинициализирован и прерывание "USART_IT_TC" полюбому сработает
// А буфер опустошается медленнее чем заполняется (скорость-то всего 9600)
while(1) // основной вечный цикл
{
if(UsartTestData()){ // Если пришли данные, то ...
uint8_t tmp=UsartGetData(); //получаем эти данные
UsartPutData(tmp+1); // Помещаем измененные данные в буфер
UsartSend(); // Вот здесь мы не знаем идет уже отправка или уже давно нет, поэтому надо толкнуть отправку
// Поидее прерывание всегда быстрее данной функции и конфликт отправки почти не возможен
}
}
}
Вот, теперь можно подключаться компом к нашему USART, врубать терминал и отправля/получать данные.
Возможные БАГИ!!!
1. На консоле компа выводятся при старте не "9876543210", какая-то ересь/ерунда/кракозябрики!
Это почти всегда несовпадение скоростей приемника и передатчика или, редко, несоответствие других настроек порта.
а) Проверяем чтобы у терминала стояли ровно такие же настройки как и у нашей discovery: скорость, размер пакета, стоп-бит, проверка четности и т.п.
б) Если все соответствует, а ересь идет, значит проблема именно со скоростью! Да, мы выставили везде 9600, ну и что? Скорость зависит от скорости на которой работает МК. Да, мы использовали штатную функцию из библиотеки для инициализации, она тоже оперирует только преднастроенными константами. Т.е. в прошивке мы указали кварц 8MHz, а стоит у нас почему-то 4MHz (например) - вот и скорость USART упала в 2 раза. Или мы забыли добавить какую-то константу и теперь штатная функция всегда считает, что мы тактируемся от 25MHz (по умолчанию). Прежде чем окончательно разобраться с тактированием, я просто комментировал "SystemInit();" и все значения по умолчанию со всех сторон совпали и все заработало. Т.е. см. выше как настраивать тактирование МК!
2. Не срабатывают прерывания!
Вот тут я долго мучался, брал якобы рабочие примеры, но работало все как-то непонятно: то работает, то нет. Оказалось все очень просто и тут вопрос именно внимательности, что и где мы пишем, а именно смотрим на названия констант. Константы USART_FLAG_TXE и USART_SR_TXE - это одно и тоже, просто выравнены названия, что там где SR - это для проверки соответствующего бита в регистре USARTx->SR, а там где FLAG - это для проверки этого же регистра, но через функцию "USART_GetFlagStatus()", что, если посмотреть ее код, полностью одно и тоже. В найденных мной примерах, почему-то именно USART_SR_TXE использовалась для включения прерывания - это НЕВЕРНО! Все что касается прерывания - это константы IT, например, USART_IT_RXNE и функции проверки прерывания и их разрешения также содержат эти IT. В частности, чтобы узнать какое именно прерывание сработало, надо использовать функцию "USART_GetITStatus()", а чтобы более не было желания использовать левые константы, можно залезть в исходники, чтобы убедиться, что логика этих констант такова, что лучше их не путать
3. Везде все правильно, но все равно что-то идет не так...
Осталось только одно - это что у вас к этим пинам что-то еще подключено физически и мешает Вашему USART передавать или получать данные, а может все сразу. Или надо брать другой USART или брать другие ноги, если конечно они тоже не заняты.
Использование Printf для ввода/вывода через USART.
Как правило везде уже присутствуют необходимый набор функций для работы printf, его только надо подключить и чуть-чуть подредактировать, чтобы указать на само устройство ввода/вывода. В CooCox`е надо в репозитории выбрать "Retarget printf", а он сам еще подключит связанную библиотеку-заглушку "C Library", а точнее "syscalls.c", где присутствуют функции минимальной поддержки для работы с периферией. Я видел одну статью, где упоно рекомендовали использовать этот файлик из RTOS, где производился прямой вывод на USART1, что в мои планы совсем не входило, да и разбираться, что за ересь там тоже не хотелось, потому я пошел штатным простым примером, до которого у народа тоже чего-то не хватает и у них ничего не компилится или еще что-то. А ведь все очень просто!!!
В "main.c" надо добавить "#include <stdio.h>", теперь можем использовать в нем "Printf()", но он не будет работать. А в нашем проекте уже появилась папочка "stdio", в которой есть файл "printf.c", откроем его на редактирование. Чтобы туда добавить вызов наших функций, надо сперва создать нам самим заголовочный файл "main.h", куда поместим:
Код: Выделить всё
char UsartPutData(char Dt);
void UsartSend();
Далее в "printf.c" добавляем "#include "main.h"". Редактируем функцию "PrintChar()", таким образом:
Код: Выделить всё
/**
* @brief Transmit a char, if you want to use printf(),
* you need implement this function
*
* @param pStr Storage string.
* @param c Character to write.
*/
void PrintChar(char c)
{
/* Send a char like:
while(Transfer not completed);
Transmit a char;
*/
while(!UsartPutData(c)){}; // Пробуем добавить символ в буфер, пока у нас это не получится успешно!
}
А чтобы забыть, что мы что-то еще должны дернуть, можно в "printf()" добавить "UsartSend();" чтобы стало вот так:
Код: Выделить всё
signed int printf(const char *pFormat, ...)
{
va_list ap;
signed int result;
/* Forward call to vprintf */
va_start(ap, pFormat);
result = vprintf(pFormat, ap);
va_end(ap);
UsartSend(); // запускаем передачу
return result;
}
Такой подход разве что ограничивает использование "printf()" на размер вывода не более размера буфера отправки, иначе "PrintChar()" может тупо зависнуть в ожидании, когда буфер освободится, а буфер не освободится пока кто-нибудь не запустит передачу. В принципе проблема натянута и имеет множество решений. Мне хватит просто не запускать "printf()" для строки длиннее 255, а много раз по чуть-чуть меня вполне устроит .
Тестовая главная процедура теперь будет выглядеть уже красивее, да и результат работы будет нагляднее:
Код: Выделить всё
int main(void)
{
SystemInit();
initAll();
/* GPIOD Periph clock enable */
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE);
/* Configure PD12, PD13, PD14 and PD15 in output pushpull mode */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13| GPIO_Pin_14| GPIO_Pin_15;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_Init(GPIOD, &GPIO_InitStructure);
printf("9876543210\n");
while(1)
{
if(UsartTestData()){
uint8_t tmp=UsartGetData();
printf("Test:\"%c\"\n",tmp);
}
}
}
Запускам "Rebuild" и "нате Вам", какие-то ошибки... У меня была ошибка при линковке, в каком-то временном файле "что-то_там.S", при том, что я такого не просил . Проблема решается выставлением оптимизатора. По умолчанию оптимизация отключена, поэтому идем в "Configuration" нашего проекта на закладку "Compile" и в поле "Optimization" выбираем ... да что угодно, лишь бы ни ее отсутствие. Единственное, что до оптимизации, можно было любые глобальные переменные как угодно определять, а теперь только с доп. идентификатором "volatile", см. счетчики начала/конца кольцевого буфера, что необходимо, чтобы переменная не оптимизировалась и всегда была доступна только по одному адресу. Переменные вроде массивов или структур обычно итак не подлежат оптимизации и последние версии "gcc" даже ругаются на директиву "volatile", применяемую для переменных, неподлежащих оптимизации, но если "gcc" не возражает, то лучше "volatile" ставить везде, где к глобальной переменной осуществляется доступ из прерывания.
Теперь проверяем: все прекрасно компилится и работает стабильно!!! Ошибка может возникнуть только, если переполнится буфер приема - пропадут последние данные. Но тут решения могут быть такие: Увеличение буфера, или увеличение скорости обработки данных, или уменьшение скорости передачи, или создание своего протокола, где будет контроль успеха передачи.
Вот теперь можно исследовать периферию STM32 далее...