Внутренняя поддержка Flash
Производитель внедрил в микроконтроллеры STM32 специальный драйвер Flash Program / Erase Controller (FPEC), который можно использовать для изменения содержимого флэш-памяти. В микроконтроллерах STM32 флэш-память организована в виде страниц, размер которых составляет 1 КБ или 2 КБ, в зависимости от общего объема памяти.
Флэш-память находится в адресном пространстве процессора, начиная с адреса 0x08000000. Подробное описание того, как обрабатывать флэш-память в микроконтроллерах STM32, можно найти в документе PM0042 под названием «Программирование флэш-памяти STM32F10xxx», который доступен на веб-сайте STMicroelectronics.
После сброса микроконтроллера FPEC защищает содержимое флэш-памяти от сохранения, поэтому нет возможности случайного повреждения ее содержимого. Чтобы внести какие-либо изменения, вы должны сначала разблокировать память. Это делается путем ввода двух значений в реестр Flash_KEYR:
KEY1 = 0x45670123
а затем
KEY2 = 0xCDEF89AB
С этого момента могут выполняться операции стирания и записи памяти, пока не произойдет ошибка, которая блокирует доступ к памяти до следующей разблокировки. Алгоритм записи во флэш-память через FPEC показан на рис. 2.
Рис. 2. Алгоритм программирования флэш-памяти в микроконтроллерах STM32
Запись в память производится 2 байтами. Сначала установите бит PG в реестре Flash_CR, информируя контроллер о том, что мы будем программировать. После этого действия вы можете изменить содержимое памяти, что требует ввода 2 байтов по адресу, где они должны быть размещены в памяти. Затем дождитесь окончания этой операции и убедитесь, что запись была произведена правильно, для чего необходимо прочитать записанные 2 байта и убедиться в их правильности — это можно сделать путем сравнения. Если ошибок не нет, то вы можете записать следующие данные таким же образом. После сохранения всей страницы вы должны удалить бит PG.
Содержимое флэш-памяти в микроконтроллерах STM32 может быть удалено двумя способами: страница за страницей или все ее содержимое. В случае загрузчика память будет удаляться постранично. Постраничный алгоритм удаления показан на рис. 3.
Рис. 3. Алгоритм стирания флэш-памяти в микроконтроллерах STM32 (постраничный)
Мы начинаем стирать содержимое памяти, устанавливая бит PER, информирующий FPEC в регистре Flash_CR о том, что страница флэш-памяти будет удалена. Затем введите адрес удаляемой страницы в регистр Flash_AR и установите бит STRT в регистр Flash_CR. В этот момент начинается физическое удаление содержимого указанной страницы памяти. На следующем этапе мы ожидаем его завершения, после чего проверяем, была ли страница удалена правильно, читая и проверяя ее содержимое.
Что вообще может DMA?
Сначала рассмотрим схему и табличку из документации, чтобы понять, как настроить DMA и какие у него есть возможности. У DMA контроллера — 7 каналов. У микроконтроллеров STM32 могут быть несколько DMA контроллеров. Конечно, они могут работать параллельно. В нашем STM32F103C8 только один 7-канальный DMA контроллер.
Из схемы и таблицы видно, что определенная периферия закреплена за определенными каналами. И, если мы используем ADC1, то мы его можем использовать только на канале 1. У нас нет возможности перенести ADC1 на канал, который нам заблагорассудится. Итак, если мы используем первый канал под ADC1, тогда TIM2_CH3 и TIM4_CH1 (согласно таблице) — в пролете. То есть, в DMA мы их уже не сможем задействовать. Для работы с UART1 нам понадобится каналы 4, 5
Обратите внимание, что, скажем, ADC2 вообще здесь не фигурирует. То есть, не любую периферию можно использовать с DMA
Комбинаций, как видим, не так много и надо все четко планировать, чтобы каналы DMA были заняты исключительно по делу. Также следует понимать, что в DMA все же есть ограничения по скорости.
Регистры USART
Регистр состояния USART_SR
CTS — сигнал о переключении контакта CTS
LBD — обнаружен сигнал BREAK в режиме LIN
TXE — данные отправлены в передатчик, регистр данных опустел
TC — данные переданы, передача завершена
RXNE — данные приняты, регистр данных полон
IDLE — обнаружен сигнал IDLE
ORE — ошибка: переполнение входного буфера — приняты новые данные, а старые ещё не прочитаны
NE — обнаружение шума на линии
FE — рассинхронизация, сильный шум на линии или принят сигнал BREAK
PE — ошибка контроля чётности принятых данных
Регистр состояния USART_DR
В 9 младших битах содержатся данные. При получении байта отсюда можно прочитать принятые данные, для отправки нужно записать данные в этот регистр.
Регистр состояния USART_BRR
DIV_Mantissa — целая часть значения USARTDIV
DIV_Fraction — дробная часть значения USARTDIV
USARTDIV = DIV_Mantissa + DIV_Fraction/16.
Baud rate = F_clk/(DIV_Fraction*16) = F_clk/(DIV_Mantissa*16 + DIV_Fraction) = F_clk/USART_BRR.
К примеру, при тактовой частоте 24МГц нужна скорость 9600 бод. Делим 24000000 на 9600, это равно 2500 = 0x9C4. Именно такое значение нужно положить в регистр BRR. Вот таблица значений BRR в зависимости от частоты процессора и требуемого бодрейта. Белые — точные значения (погрешность 0%), жёлтые — менее точные (погрешность 0 — 0.1%), красные — самые неточные (погрешность 0.1 — 0.3%). Однако, даже красные значения можно применять в хобби-устройствах.
Регистр состояния USART_CR1
UE — включение USART: 0 — выключен, 1 — включен.
M — длина слова данных: 0 — 8 бит, 1 — 9 бит. Не должен меняться во время приёма/передачи.
WAKE — способ пробуждения модуля USART
PCE — включение контроля чётности для приёма и передачи. Делает старший бит (8 или 9) битом чётности.
PS — полярность бита чётности
PEIE — включение прерывания по установке бита PE регистра SR, т.е. при ошибке чётности
TXEIE — включение прерывания по установке бита TXE регистра SR, т.е. при начале передачи данных
TCIE — включение прерывания по установке бита TC регистра SR, т.е. при окончании передачи данных
RXNEIE — включение прерывания по установке бита ORE регистра SR, т.е. при переполнении входного буфера
IDLEIE — включение прерывания по установке бита IDLE регистра SR, т.е. при приёме сигнала IDLE
TE — включение передатчика
RE — включение приёмника
RWU — перевод приёмника в спящий режим: 0 — активный, 1 — спящий
SBK — передача сигнала BREAK. Сбрасывается железом после передачи.
Регистр состояния USART_CR2
LINEN — включение режима LIN
STOP — программирование количества стоповых бит. 00 — 1 бит, 01 — 0.5 бит, 10 — 2 бит, 11 — 1.5 бит.
CLKEN — использование пина CK, включение синхронного режима
CPOL — полярность сигналов пина CK: 0 — высокое значение при передаче, 1 — низкое.
CPHA — фаза сигналов CK: 0 — первый перепад CK в первый бит передачи, 1 — второй.
LBCL — передача сигнала CK по передаче последнего бита (MSB)
LBDIE — включение прерывания по установке бита LBD регистра SR
LBDL — длина сигнала BREAK в LIN: 0 — 10, 1 — 11
ADD — адрес узла USART
Регистр состояния USART_CR3
CTSIE — включение прерывания по установке бита CTS регистра SR
CTSE — включение сигнала CTS, т.е. аппаратного управления потоком
RTSE — включение сигнала RTS и генерирование прерывания по установке бита RTS
DMAT — использование DMA для передачи
DMAR — использование DMA для приёма
SCEN — включение режима Smartcard
NACK — использование сигнала NACK для режима Smartcard
HDSEL — полудуплексный режим для однопроводного режима
IRLP — выбор режима пониженного потребления IrDA
IREN — включение режима IrDA
EIE — включение прерывания при возникновении ошибки
Examples for UART DMA for TX (and optionally included RX)
- Application is using DMA in normal mode to transfer data
- Application is always using ringbuffer between high-level write and low-level transmit operation
- DMA TC interrupt is triggered when transfer has finished. Application can then send more data
Demo application for debug messages
This is a demo application available in folder.
Its purpose is to show how can application implement output of debug messages without drastically affect CPU performance.
It is using DMA to transfer data (no CPU to wait for UART flags) and can achieve very high or very low data rates
- All debug messages from application are written to intermediate ringbuffer
- Application will try to start & configure DMA after every successfive write to ringbuffer
- If transfer is on-going, next start is configured from DMA TC interrupt
As a result of this demo application for STM32F413-Nucleo board, observations are as following:
- Demo code sends bytes every second at bauds, which is approx .
- With DMA disabled, CPU load was , in-line with time to transmit the data
- With DMA enabled, CPU load was
- DMA can be enabled/disabled with macro configuration in
General about UART
UART in STM32 allows customers to configure it using different transmit()/receive() modes:
- Polling mode (no DMA, no IRQ)
- Application is polling for status bits to check if any character has been transmitted/received and read it fast enough in order to not-miss any byte
- P: Easy to implement
- C: Easy to miss received characters in bursts
- C: Works only for low baudrates, or lower
- C: Application must periodically (with high frequency) check for new characters, usually not always possible at complex systems
- Interrupt mode (no DMA)
- UART triggers interrupt and CPU jumps to service routine to handle each received byte separately
- P: Commonly used approach in embedded applications
- P: Works well with common baudrates, , up to bauds
- C: Interrupt service routine is executed for every received character
- C: May stall other tasks in high-performance MCUs if interrupts are triggered for every character
- C: May stall operating system when receiving burst of data, interrupt priority must be higher than operating system maximum is
- DMA mode
- DMA is used to transfer data from USART RX data register to user memory on hardware level. No application interaction is needed at this point except processing received data by application once necessary
- P: Transfer from USART peripheral to memory is done on hardware level without CPU interaction
- P: Can work very easily with operating systems
- P: Optimized for highest baudrates and low-power applications
- P: In case of big bursts of data, increasing data buffer size can improve functionality
- C: Number of bytes to transfer must be known in advance by DMA hardware
- C: If communication fails, DMA may not notify application about all bytes transferred
Every STM32 have at least one () UART IP available and at least one () DMA controller.
For transmitting data, no special features on top of basic are necessary, except DMA availability. We will use default features to implement very efficient transmit system using DMA.
This is not the case for data receive operation. When implementing DMA receive, application would need to understand when (possible) burst of data received to MCU finished and react immediatelly. This is especially true when UART is used for system communication where it has to react immediately. STM32s have a capability (not all) in UART to detect when RX line has not been active for period of time. This is achieved using one of available features:
- IDLE LINE: This is an event, triggered when RX line has been in idle state (normally high state) for frame time, after last received byte. Frame time is based on baudrate. Higher baudrate, lower frame time for single byte to be received.
- RTO (Receiver Timeout): This event is triggered when line has been in idle state for programmable time. It is fully configured by UART.
Both events can trigger an interrupt.
An example: To transmit byte at bauds, it takes approximately (for easier estimation) ; for it would be in total. IDLE line event triggers an interrupt for application when line has been in idle state for frame time (in this case ) after third byte has been received.
This is a real experiment demo using STM32F4 and IDLE line. After IDLE line is triggered, data are echoed back (loopback mode):
- Application receives bytes, takes approx at bauds
- RX goes to high state (yellow rectangle) and UART RX detects that it has been idle for at least frame
- IDLE line interrupt is triggered at green arrow
- Application echoes data back from interrupt
Настройка порта GPIO
Итак, с регистрами разобрались, настало время практики. Все примеры в этой статье для микроконтроллера STM32F103C8. В моем распоряжении есть вот такая отладочная плата:
На ней установлен кварцевый резонатор на 8 МГц и светодиод на порту PB12. Вот с помощью этого светодиода мы и устроим Hello, World!
Задача ясна: настраиваем PB12 на выход в режиме push-pull и с помощью регистра ODR дергаем 12-й пин порта GPIOB туда-сюда! Но мы забыли об одной маленько детали: RCC. Дело в том, что по-умолчанию после сброса микроконтроллера все периферийные модули отключены от источника тактового сигнала, в том числе и GPIO. А подать тактирование можно с помощью регистров RCC. В 3-ей части я про это говорил. Для начала нужно определить, к какой шине у нас подключен GPIOB. Открываем даташит на микроконтроллер, ищем вот эту таблицу:
Рис. 8. Таблица шин и периферийных устройств
GPIOB у нас подключен к шине APB2. Идем в Reference manual, открываем раздел про RCC, переходим к пункту 7.3.7 APB2 peripheral clock enable register (RCC_APB2ENR). С помощью этого регистра можно подать тактовый сигнал на устройства шины APB2:
Рис. 9. Регистр RCC_APB2ENR
В регистре RCC_APB2ENR много флагов для разной периферии, в том числе и для нашего GPIOB, флаг называется IOPBEN. Перед началом инициализации PB12 нам надо установить этот бит в единицу.
Поехали программировать! За основу возьмем проект из 2-й части: https://github.com/DiMoonElec/stm32f103c8_empty_project. Создадим функцию инициализации порта:
void PortInit(void) { }
GPIOB
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; //Включаем тактирование порта GPIOB
PB12CRH:
GPIOB->CRH &= ~(GPIO_CRH_MODE12 | GPIO_CRH_CNF12); //для начала все сбрасываем в ноль //MODE: выход с максимальной частотой 2 МГц //CNF: режим push-pull GPIOB->CRH |= (0x02 << GPIO_CRH_MODE12_Pos) | (0x00 << GPIO_CRH_CNF12_Pos);
void PortInit(void) { RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; //Включаем тактирование порта GPIOB GPIOB->CRH &= ~(GPIO_CRH_MODE12 | GPIO_CRH_CNF12); //для начала все сбрасываем в ноль //MODE: выход с максимальной частотой 2 МГц //CNF: режим push-pull GPIOB->CRH |= (0x02 << GPIO_CRH_MODE12_Pos) | (0x00 << GPIO_CRH_CNF12_Pos); }
ODRPB12
void PortSetHi(void) { GPIOB->ODR |= (1<<12); }
void PortSetLow(void) { GPIOB->ODR &= ~(1<<12); }
GPIOB->ODR |= (1<<12)STM32
void PortSetHi(void) { GPIOB->BSRR = (1<<12); } void PortSetLow(void) { GPIOB->BRR = (1<<12); }
(1<<12)0x1000BSRRBRRрис. 5, 6
Ни и простой main() для проверки:
void main() { int i; PortInit(); for(;;) { PortSetHi(); for(i=0; i<0x40000; i++) ; PortSetLow(); for(i=0; i<0x40000; i++) ; } }
BP12«Hello, World!»
Давайте теперь настроим какой-нибудь вывод порта, например PB15, на вход с подтяжкой к питанию. При подключении PB15 к минусу, у нас будет зажигаться светодиод. Задача ясна, преступаем к реализации. В PortInit() добавим пару строк:
/// Настраиваем PB15 на вход с подтяжкой к питанию /// GPIOB->CRH &= ~(GPIO_CRH_MODE15 | GPIO_CRH_CNF15); //MODE: вход, оставляем в нуле //CNF: вход с pull-up / pull-down GPIOB->CRH |= (0x00 << GPIO_CRH_MODE15_Pos) | (0x02 << GPIO_CRH_CNF15_Pos); GPIOB->ODR |= (1<<15); //Включаем подтяжку вверх
PB12MODE/CNFODRODRCNF=10Input with pull-up / pull-downODRReference manual
Рис. 10. Таблица конфигурации порта
Функция PortInit() приобретает такой вид:
void PortInit(void) { RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; //Включаем тактирование порта GPIOB /// Настраиваем PB12 на выход /// GPIOB->CRH &= ~(GPIO_CRH_MODE12 | GPIO_CRH_CNF12); //для начала все сбрасываем в ноль //MODE: выход с максимальной частотой 2 МГц //CNF: режим push-pull GPIOB->CRH |= (0x02 << GPIO_CRH_MODE12_Pos) | (0x00 << GPIO_CRH_CNF12_Pos); /// Настраиваем PB15 на вход с подтяжкой к питанию /// GPIOB->CRH &= ~(GPIO_CRH_MODE15 | GPIO_CRH_CNF15); //MODE: вход, оставляем в нуле //CNF: вход с pull-up / pull-down GPIOB->CRH |= (0x00 << GPIO_CRH_MODE15_Pos) | (0x02 << GPIO_CRH_CNF15_Pos); GPIOB->ODR |= (1<<15); //Включаем подтяжку вверх }
PB15IDR
int ReadPort(void) { if(GPIOB->IDR & (1<<15)) return 1; return 0; }
IDR
В этом случае main() будет выглядеть вот так:
void main() { PortInit(); for(;;) { if(ReadPort()) PortSetHi(); else PortSetLow(); } }
stm32f103c8PB12PB15PP12PB15PB15ReadPort()PB12
На этом все, продолжение следует! Продолжение.
Idle Line or Receiver Timeout events
STM32s have capability in UART to detect when RX line has not been active for period of time. This is achieved using methods:
- IDLE LINE event: Triggered when RX line has been in idle state (normally high state) for frame time, after last received byte. Frame time is based on baudrate. Higher baudrate means lower frame time for single byte.
- RTO (Receiver Timeout) event: Triggered when line has been in idle state for programmable time. It is fully configured by firmware.
Both events can trigger an interrupt which is an essential feature to allow effective receive operation
An example: To transmit byte at bauds, it takes approximately (for easier estimation) ; for it would be in total.
IDLE line event triggers an interrupt when line has been in idle state for frame time (in this case ), after third byte has been received.
This is a real experiment demo using STM32F4 and IDLE LINE event. After IDLE event is triggered, data are echoed back (loopback mode):
- Application receives bytes, takes approx at bauds
- RX goes to high state (yellow rectangle) and UART RX detects it has been idle for at least frame time (approx )
- IDLE line interrupt is triggered at green arrow
- Application echoes data back from interrupt context
Обработка ошибок на уровне пакетов и таймаутов
- Если код команды не соответствует проверочному инверсному коду, то начало пакета не засчитывается, и поиск новой команды начинается со следующих байт.
- Если принят пакет с размером дополнительной информации больше чем внутренний буфер, то пакет игнорируется, и поиск новой команды начинается со следующих после размера байт.
- Если принятая в теле пакета CRC32 не равна фактически расчётной, то содержимое пакета игнорируется и поиск новой команды начинается с следующих после CRC32 байт.
- Если приходят байты, но сигнатура начала пакета не задетектирована. То эти байты считать отладочными текстовыми сообщениями и накапливать до кода 13 (перевод строки), а после этого кода выводить в отладочную консоль.
- Если с момента приёма последнего пакета прошло более 500мс, то загрузчик сбрасывается в изначальное состояние. Пакет, который не успел приняться до конца, игнорируется и так-же сбрасывается. О таймауте Устройство сообщает пакетом с специальным кодом команды «таймаут».
- При запуске загрузчика генерируется другой пакет со специальным кодом «перезагрузка».
Регистры USART
Status register (USART_SR) — регистр статуса
TXE: регистр передатчика пуст. Этот бит устанавливается аппаратно, когда содержимое регистра передатчика TDR (TDR не доступен напрямую из программы, но туда попадают данные при записи в USART_DR) было передано в сдвиговой регистр. Если в USART_CR1 был установлен бит разрешения прерывания TXEIE, то в этот момент генерируется запрос прерывания USART. TXE сбрасывается при записи значения в регистр данных USART_DR.
TC: передача завершена. Этот бит устанавливается аппаратно, если UART завершил передачу данных, при этом бит TXE установлен в единицу. Этот бит может быть полезен для реализации интерфейса RS485 для переключения направления драйвера RS485. Если в регистре USART_CR1 установлен бит TCIE, то генерируется прерывание USART при установке бита TC. Бит TC сбрасывается следующей программной последовательностью: чтение регистра USART_SR с последующей записью в регистр USART_DR. Кроме того, бит TC можно сбросить записью в него значения 0, но это рекомендуется производить только в режиме совместной работы с DMA.
RXNE: регистр приемника не пуст. Этот бит устанавливается в единицу, когда содержимое сдвигового регистра приемника передается в регистр данных USART. Если в регистре USART_CR1 установлен бит RXNEIE, то генерируется запрос прерывания USART. Бит RXNE сбрасывается при чтении регистр данных USART_DR. Кроме того, RXNE можно сбросить записью в него значение 0, но это рекомендуется производить только в режиме совместной работы с DMA.
ORE: ошибка переполнения. Устанавливается в 1, если данные в сдвиговом регистре приемника готовы к передаче в регистр данных, но при этом установлен бит RXNE. Иными словами, мы уже получили очередной байт по USART, но еще не прочитали предыдущий. Если в регистре USART_CR1 установлен флаг RXNEIE, то генерируется запрос прерывания USART. Бит ORE сбрасывается следующей программной последовательностью: чтение регистра USART_SR с последующим чтением регистра USART_DR.
Data register (USART_DR) — регистр данных
DR: данные. Этот регистр содержит 2 теневых регистра: TDR и RDR. При чтении из DR будет прочитано значение регистра данных приемника RDR, при записи в DR значение будет записано в регистр данных передатчика TDR. Если используется контроль четности (бит PCE в регистре USART_CR1 установлен в 1), то при записи в DR значение старшего бита будет игнорироваться, так как при передаче он будет заменен битом четности. При приеме с включенным контролем четности старший бит будет содержать бит четности.
Baud rate register (USART_BRR) — регистр скорости передачи данных USART
Регистр BRR содержит коэффициент деления, который задает скорость передачи данных по USART.
BRR = (uint16_t)(BUS_FREQ / BAUD)
где BUS_FREQ — частота шины, на которой висит данный USART
BAUD — желаемая скорость передачи данных.
Control register 1 (USART_CR1) — регистр конфигурации 1
UE: включить USART.
- 0: предделители USART и его выходы отключены
- 1: USART включен
M: длина слова данных. Этот бит определяет длину передаваемых данных. Устанавливается и очищается программно.
- 0: 1 старт-бит, 8 бит данных, n стоп-бит
- 1: 1 старт-бит, 9 бит данных, n стоп-бит
PCE: разрешить контроль четности. Устанавливается и очищается программно
PS: выбор типа контроля четности. Этот бит выбирает вариант контроля четности, если установлен бит PCE. Устанавливается и очищается программно.
- 0: Even
- 1: Odd
TXEIE: разрешить прерывание при опустошении буфера передатчика. Если установлен в 1, то генерируется запрос прерывания USART при установке бита TXE регистра USART_SR.
TCIE: разрешить прерывания окончания передачи. Если 1, то генерируется запрос прерывания USART при установке флага TC в регистре USART_SR.
RXNEIE: разрешить прерывание при появлении данных в регистре приемника. Если 1, то генерируется запрос прерывания USART при установке флага RXNE или ORE в регистре USART_SR.
TE: включить передатчик USART
RE: включить приемник USART
Control register 2 (USART_CR2) — регистр конфигурации 2
STOP: количество STOP-битов
- 00: 1 стоп-бит
- 01: 0.5 стоп-бита
- 10: 2 стоп-бита
- 11: 1.5 стоп-бита
0.5 и 1.5 стоп-бита не доступны для UART4 и UART5 (UART4 и UART5 отсутствуют в микроконтроллере STM32F103C8)
Заключение
В общем и целом перед поставленной задачей товарищи студенты справились.
При включенном принудительном inline, весь код проекта занимает 1600 байт без оптимизации. Сделаны две команды: запись и чтение 12 параметров во Flash микроконтроллера. В проекте, можно настроить драйвер, чтобы он работал в синхронном режиме, можно в асинхронном. Можно подключать любое количество подписчиков, к любому UART. Собственно все задачи были выполнены и работа тянет на отлично 🙂
Да, затрачено на кодирование было 2 целых дня (думаю часов 20 в сумме). Полагаю, из-за того, что архитектура мною уже была разработана на практических занятиях, а реализация — это дело уже не таке сложное.
Код был проверен мною в PVS-Studio. Изначально были найдены 4 предупреждения.
Все предупреждения уже не помню, отчет не сохранил: но точно были V2516 и V519, ошибки не критичные, но точно так делать не надо было 🙂 Все исправлено, кроме V2516, он указывает на код, который используется для отладки, там поставил FIXME:.