Настройка UART в STM32 и проблемы двоично-десятичной арифметики

Данная заметка в первую очередь будет интересна без(д)умным поклонниками библиотеки SPL от конторы STMicroelectronics.

Предыстория
Имеется некое устройство на STM32F105 с прошивкой на основе стандартных фирменных библиотек. Устройство должно общаться с внешним миром посредством интерфейса UART на скорости 115200 бод в связке с мостом CP2102. Кварцевый резонатор 11,0592 МГц, частота ядра х4.

Была обнаружена интересная особенность. Если указать частоту кварцевого генератора (параметр HSE_VALUE) неточно, скажем, 11059000 Гц с ошибкой в 200 Гц, то UART настраивался неправильно и работал с завышенной частотой. В то же время для значения HSE_VALUE=11059200 — всё в порядке. Налицо некорректный расчёт делителя тактовой частоты UART_BRR.

Что это, баг это или фича — давайте разберёмся.

UART в STM32 имеет дробный делитель тактовой частоты
Дробный делитель — это хорошо. Многие, вероятно, сталкивались с большой погрешностью BaudRate у контроллера AVR, при которой некоторые привередливые интерфейсы (типа CP2102) отказывались надёжно работать. Поэтому и были придуманы кварцы с хитрой частотой (типа 11,0592 МГц), которую можно поделить нацело для обеспечения требуемой точной скорости UART. А с дробным делителем можно получить существенно меньшую погрешность при любой (в разумных пределах, конечно) удобной частоте кварцевого генератора.

Однако. Рассчитать коэффициенты дробного делителя заметно сложнее. Нужно что-то на что-то делить и умножать, отдельно рассчитывать целую и дробную часть. Не запутаться бы… Собственно, в SPL всё за нас уже сделано, и мы не задумываясь пишем:

USART_InitStructure.USART_BaudRate = 115200;
...
USART_Init(USART1, &USART_InitStructure);

Стоп! Здесь спрятана мина!

А сработает она или нет, дело случая.

Смотрим внимательнее. Согласно даташиту на STM32,

и далее приводится несколько примеров, как это значение USARTDIV преобразовать в целую и дробную часть регистра BRR:

Ну и ребята-индусы, писавшие SPL, честно реализовали данный алгоритм:
/* Determine the integer part */
integerdivider = ((25 * apbclock) / (4 * (USART_InitStruct->USART_BaudRate)));   
tmpreg = (integerdivider / 100) << 4;

/* Determine the fractional part */
fractionaldivider = integerdivider - (100 * (tmpreg >> 4));
tmpreg |= ((((fractionaldivider * 16) + 50) / 100)) & ((uint8_t)0x0F);

/* Write to USART BRR register */
USARTx->BRR = (uint16_t)tmpreg;

Вам понятно? Мне тоже нет.

Сначала считаем целую часть:
integerdivider = ((25 * apbclock) / (4 * (USART_InitStruct->USART_BaudRate)));
или лучше вот так:
integerdivider = (apbclock * 100) / (16 * (USART_InitStruct->USART_BaudRate)));
Это похоже на первую формулу, а коэффициент 100 нужен для работы с двумя дробными десятичными разрядами при помощи целочисленной арифметики (прям как в Example 2).

Далее
tmpreg = (integerdivider / 100) << 4;
отбрасываются эти самые дробные разряды и значение INT занимает нужную позицию (см. описание регистра UART_BRR).

Затем
fractionaldivider = integerdivider - (100 * (tmpreg >> 4));
tmpreg сдвигается то влево, то вправо… ну ничего, компилятор сам оптимизирует если захочет… Здесь fractionaldivider содержит дробную часть делителя, умноженную на 100. Если рассматривать Example 2, то это будет 62.

(вы ещё здесь?)

И последней строчкой
tmpreg |= ((((fractionaldivider * 16) + 50) / 100)) & ((uint8_t)0x0F);
дробная часть приводится к нужному формату:
DIV_Fraction = ((62*16)+50)/100 = 10.

Замечу, что в DIV_Fraction попадают только 4 бита! А все остальные отбрасываются. На что в принципе имеем полное право, так как дробная часть числа должна быть меньше единицы, — на то она и дробная. И в понимании делителя UART значение DIV_Fraction не должно превышать 15.

В моём случае получилось DIV_Fraction=16, старший бит был успешно отброшен и в INT не попал. Ошибочка. Погрешность BR получилась около 4%.

А теперь вопрос к уважаемым писателям-мозгозасирателям даташита на STM32Fxxx: какого х@# при расчёте BRR используется десятичная арифметика? Контроллер ведь работает в двоичной?! А уважаемые индусы чётко следуют писанию и реализуют то, что в нём написано, в своих SPL- и HAL-мемуарах.

Поясняю в картинках:

Чтобы полноценно работать с представленным дробным числом при помощи целочисленного арифмометра, необходимо и достаточно домножить его на 2^4=16! А никак не на 100. И тогда расчёт сведётся всего лишь к одному действию
USART_BBR = (fck * 16)/(16 * BaudRate);

или проще
USART_BBR = fck / BaudRate;

И всё! Не нужны мутные сдвиги, умножения и деления. Ну почему уважаемая ST не привела эту формулу, а запудрила мозги своими дробными примерами?

Если уж быть совсем точным, то желательно округлить результат то ближайшего целого. Для этого к результату нужно прибавить половину младшего разряда и отбросить дробную часть… Например вот так:
temp = (fck * 2) / BaudRate;
temp += 1;
temp >>= 1;
USART_BBR = temp;

или, действительно, ещё правильнее и короче:
temp = fck + (BaudRate >> 1);
temp /= BaudRate;
USART_BBR = temp;

Заключение
Как говорится, людям свойственно ошибаться. Простим их. И продолжим пользоваться библиотекой SPL/HAL, но внимательно и осторожно, почаще заглядывая в её исходники.

Кстати, такой же некорректный расчёт заложен и в новую библиотеку STM32Cube. Только вычисления зачем-то обёрнуты в макросы, поэтому понять, что там не так, ещё сложнее.
  • +8
  • 15 сентября 2014, 10:48
  • MikeSmith

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

RSS свернуть / развернуть
про это тут в комментариях писали
0
Спасибо, да, возможно, пропустил. Обычно в статьях пишут про два равноценных способа расчёта ("… мне больше нравится такой-то..." — ну нравится, и что?) или вопрос расчёта просто опускается. А из комментариев полезная информация добывается тяжелее.
0
пришла в голову мысль: а не создать ли Большой Открытый репозиторий с SPL, чтобы можно было исправления по таким ошибкам выкладывать в общий доступ?
+1
  • avatar
  • anper
  • 15 сентября 2014, 11:23
есть в coocox, выкладывай чего хочешь, не выходя из IDE
0
Напишите в ST, пусть поправят. Вдруг «А мужики-то не знают!»
+3
Ну-ну. Год назад писал в TI по поводу опечатки в даташите. До сих пор не поправили.
0
ДА лучше бы индусы замутили генератор кода как у Silabs для x51. Красота, все просто, наглядно и без лишних движений.
На счет округления, так будет лучше
BBR = f/b +0.5 (округление) => (f+(b>>2))/b
0
ну тогда уж BBR = f/b +0.5 (округление) => (f+(b>>1))/b
0
Да блин деление там стояло у меня заменил на сдвиг коряво)))
0
да я так и подумал)
0
Да уж. В исходном коде надо было либо не округлять (не прибавляя 50/100), либо после округления дробную часть просто приплюсовать к целой, а не OR'ить через маску, всё хотя бы работало.
Ну тут хотя бы ошибка с первого взгляда и не заметна. Вот тут показывал код из примеров к плате Дискавери, как можно было написть такое, вообще не представляю.
0
  • avatar
  • ACE
  • 15 сентября 2014, 15:57
А теперь вопрос к уважаемым писателям-мозгозасирателям даташита на STM32Fxxx: какого х@# при расчёте BRR используется десятичная арифметика?
это все обрезование виновато и мудематика
в природе ведь никаких десятичных и отрицательных чисел нет и реальный контроллер это подтверждает

и достаточно домножить его на 2^4=16!
знал бы ты сейчас, что ты только что сказал незадумываясь
2^4 и есть умножение, краткая запись

вспомнился быдловуз и урок алгоритмов, когда изучали десятичные дроби
крышу всем посносило
видно же, что какую-то абстгакцию пытаются впихнуть в реально работающую вещь на правильной математике и оно туда еле входит, как и отрицательные числа
когда мы писали зачет, то я там тоже двигал биты туда-сюда и конечно же все просрал
препод еще хитрожопый попался и сделал все рекурсивно, что если первое задание неправильно, то и все остальные неправильны автоматом
естественно я не сдал и половина не сдала, а другая сдала, но если бы их спросили, как и что работает, то никто бы не ответил, т.к. тупо запомнили и воспроизвели
а кто пытался головой работать, за 1.5 часа ничего не успел
потом вот такие быдловузники и пишут даташиты и врят ли это индусы, ведь там еще и орфографию соблюдать надо, а они говорят на индише
0
Спасибо за статью. Я и раньше был не в восторге от документации на STM32, а теперь мнение еще больше укрепилось.
С другой стороны, кто все разобрал, увяз, вынырнул, после такой проделанной работы к каким-нить TI или LPC не сбежит и будет верен STM32 до конца.
0
  • avatar
  • igorp
  • 17 сентября 2014, 11:27
В LPC от NXP ещё интереснее. Там 100% похоже на алгоритм Брезенхема.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.